# LimiLake docs > Multi-tenant B2B data lake platform: workspaces hold lakehouses, code lives as functions and apps, and the limilake CLI/SDK runs identically in the sandbox and on your own machine. The concept docs below are the same corpus the limilake CLI bundles offline. ## Install the current CLI release - Resolve stable: `VERSION=$(curl -fsSL https://packages.limilake.com/cli/channels/stable)` - Quick installer: `curl -fsSL https://get.limilake.com/ | sh` - Exact installer: `curl -fsSL https://get.limilake.com/ | sh -s -- --version "$VERSION"` - Immutable manifest: `https://packages.limilake.com/cli/v$VERSION/manifest.json` - Internal GitHub Release audit (repository access required): `https://github.com/liminityab/limilake-v2/releases/tag/cli/v$VERSION` - Version-matched Agent context: `https://packages.limilake.com/cli/v$VERSION/llms.txt` - Supported targets: Linux AMD64/ARM64 and macOS AMD64/ARM64; Windows is not supported. - Workspace repositories use ordinary Git: `limilake workspace clone `, then `git fetch`, `git switch`, `git pull`, and `git push`. - The stable channel may deliberately roll back. Resolve it live instead of inferring it from the greatest published version. # Getting started LimiLake is a multi-tenant data lake platform: **workspaces** hold **lakehouses**, lakehouses hold **schemas**, and schemas hold **tables** and **files**. Code lives as **functions** (`.py`) and **apps** (`.app.tsx`) in the workspace git repo. ## 1. Install the CLI The supported CLI is one signed `limilake` binary. It does not require Python, Node.js, or a repository checkout. The first public matrix is: | Operating system | Architectures | | --- | --- | | Linux | AMD64 (`x86_64`) and ARM64 (`aarch64`) | | macOS | AMD64 (Intel) and ARM64 (Apple Silicon) | Windows is not supported by the initial release. For the stable channel, use the verified convenience installer: ```bash curl -fsSL https://get.limilake.com/ | sh limilake --output json version ``` The version command reports the semantic version, exact source commit, Go toolchain, operating system, and architecture. To review the installer before running it, or to install into a managed path: ```bash INSTALLER=$(mktemp) curl -fsSL https://get.limilake.com/ -o "$INSTALLER" sh -n "$INSTALLER" sh "$INSTALLER" --channel stable --install-dir "$HOME/.local/bin" ``` Use `--no-modify-path` when your shell profile or managed environment owns `PATH`. Resolve and install the exact current stable version with immutable version selection: ```bash VERSION=$(curl -fsSL https://packages.limilake.com/cli/channels/stable) curl -fsSL https://get.limilake.com/ | sh -s -- --version "$VERSION" limilake --output json version ``` The selected version's immutable manifest, signatures, direct archives, SBOMs, provenance, source, notices, and Agent context are published under: ```text https://packages.limilake.com/cli/v$VERSION/ ``` The GitHub Release for `cli/v$VERSION` is an internal audit record for authorized repository readers. It names the source commit, public-key fingerprints, artifact digests, and native acceptance evidence, but it is not a public trust anchor. Released version paths are immutable. For an external trust bootstrap, obtain the approved public-key digest through an independently controlled channel; the package origin's copy is not its own trust anchor. ```text https://github.com/liminityab/limilake-v2/releases/tag/cli/v$VERSION ``` ### Update, rollback, replace, or uninstall Updates are explicit. Ordinary commands do not check for updates or send usage telemetry: ```bash limilake update --channel stable ``` To roll back, select an earlier verified immutable version. A failed download, signature check, digest check, or executable replacement leaves the current binary usable: ```bash limilake update --version limilake --output json version ``` For a manual replacement after completing the direct verification below, stage the verified binary beside the destination and move it into place: ```bash mkdir -p "$HOME/.local/bin" REPLACEMENT=$(mktemp "$HOME/.local/bin/.limilake.XXXXXX") install -m 0755 "$VERIFY_DIR/limilake" "$REPLACEMENT" mv -f "$REPLACEMENT" "$HOME/.local/bin/limilake" ``` To uninstall a per-user installation, first confirm the resolved path, then remove only that executable. Project files, profiles, and the managed Build toolchain cache are separate and are not removed automatically: ```bash command -v limilake rm "$HOME/.local/bin/limilake" ``` The CLI keeps its exact version-matched Node, pnpm, Python, and uv Build toolchain under the operating system user cache at `/limilake/build-toolchains/v1/cli-v/-/`. Inspect it without network access or disk mutation: ``` limilake toolchain doctor limilake --output json toolchain doctor ``` If the report says `missing`, `invalid`, or `incompatible`, restore the exact signed cohort atomically: ``` limilake toolchain repair ``` Repair verifies a complete sibling replacement before changing the selected cache. A failed repair leaves any previously selected valid toolchain intact. Use `limilake toolchain repair --offline` when network access is prohibited; it repairs local selection state when possible and otherwise returns a stable JSON diagnostic with the exact next action when global `--output json` is selected, instead of contacting the package origin. Do not delete the cache as a first repair step. If `doctor` reports changed bytes, a missing runtime file, or an interrupted replacement, run `repair`; it restores the exact signed cohort and cleans only the version-scoped interrupted state. Then verify the recovered path without network access: ```text limilake --output json toolchain doctor limilake build --offline ``` CLI updates and rollbacks keep separate version-scoped toolchain entries. After selecting an earlier release, inspect that exact version's cache before building: ```text limilake update --version limilake --output json toolchain doctor limilake toolchain repair # only when that exact cache is unavailable ``` Every signed CLI publication is gated by clean-machine Builds on Linux AMD64/ARM64 and macOS AMD64/ARM64. The release record binds one witness per target to the exact CLI archive, CLI SBOM, Build-toolchain bundle, toolchain SBOM, manifest, and provenance identities. Those witnesses exercise acquisition, reuse, offline mode, doctor, corruption/interruption repair, update, and rollback without a repository checkout or host Node, pnpm, Python, uv, or `workspace-build`. These cache commands govern an ordinary signed standalone CLI installation. The hosted LimiLake Agent image deliberately sets an image-owned Build Engine override and supplies its tools from the pinned image. In that environment, `limilake build` does not use this managed cache, and `toolchain doctor` or `repair` does not describe or change the active Build path. ### Verify a direct download For a managed or manual exact-version install, obtain the approved public-key digest through an independently controlled channel before trusting the package origin's copy. Authorized maintainers may use the private GitHub Release as an additional internal audit record. Linux verifies the raw Ed25519 signature with OpenSSL: ```bash VERSION=$(curl -fsSL https://packages.limilake.com/cli/channels/stable) BASE="https://packages.limilake.com/cli/v${VERSION}" VERIFY_DIR=$(mktemp -d) curl -fsSL "$BASE/release-public-key.pem" -o "$VERIFY_DIR/release-public-key.pem" curl -fsSL "$BASE/release-public-key.sha256" -o "$VERIFY_DIR/release-public-key.sha256" sha256sum "$VERIFY_DIR/release-public-key.pem" (cd "$VERIFY_DIR" && sha256sum -c release-public-key.sha256) curl -fsSL "$BASE/manifest.json" -o "$VERIFY_DIR/manifest.json" curl -fsSL "$BASE/manifest.json.sig" -o "$VERIFY_DIR/manifest.json.sig.b64" openssl base64 -d -A -in "$VERIFY_DIR/manifest.json.sig.b64" -out "$VERIFY_DIR/manifest.json.sig" openssl pkeyutl -verify -pubin \ -inkey "$VERIFY_DIR/release-public-key.pem" -rawin \ -in "$VERIFY_DIR/manifest.json" -sigfile "$VERIFY_DIR/manifest.json.sig" ``` Stock macOS uses OpenSSH rather than Apple LibreSSL for the same signed manifest. Compare the SHA-256 of `release-public-key.pub` with the approved digest from that independently controlled channel, then verify the SSHSIG namespace: ```bash VERSION=$(curl -fsSL https://packages.limilake.com/cli/channels/stable) BASE="https://packages.limilake.com/cli/v${VERSION}" VERIFY_DIR=$(mktemp -d) if ! curl -fsSL "$BASE/release-public-key.pub" \ -o "$VERIFY_DIR/release-public-key.pub"; then echo "Release $VERSION predates stock macOS direct verification; use the verified installer or select a newer exact release." >&2 exit 1 fi curl -fsSL "$BASE/manifest.json" -o "$VERIFY_DIR/manifest.json" curl -fsSL "$BASE/manifest.json.sshsig" -o "$VERIFY_DIR/manifest.json.sshsig" shasum -a 256 "$VERIFY_DIR/release-public-key.pub" printf 'limilake-release namespaces="limilake-cli-manifest" ' \ > "$VERIFY_DIR/allowed-signers" cat "$VERIFY_DIR/release-public-key.pub" >> "$VERIFY_DIR/allowed-signers" ssh-keygen -Y verify \ -f "$VERIFY_DIR/allowed-signers" \ -I limilake-release \ -n limilake-cli-manifest \ -s "$VERIFY_DIR/manifest.json.sshsig" \ < "$VERIFY_DIR/manifest.json" ``` After the manifest signature passes, select the archive for the machine and verify its signed digest before extraction: ```bash case "$(uname -s):$(uname -m)" in Linux:x86_64) OS=linux; ARCH=amd64 ;; Linux:aarch64) OS=linux; ARCH=arm64 ;; Darwin:x86_64) OS=darwin; ARCH=amd64 ;; Darwin:arm64) OS=darwin; ARCH=arm64 ;; *) echo "Unsupported platform" >&2; exit 1 ;; esac ARTIFACT="limilake_${VERSION}_${OS}_${ARCH}.tar.gz" DIGEST=$(awk -v target="cli/v${VERSION}/${ARTIFACT}" ' index($0, "\"path\": \"" target "\"") { matched = 1; next } matched && index($0, "\"sha256\": \"") { line = $0 sub(/^.*"sha256": "/, "", line) sub(/".*$/, "", line) print line exit } ' "$VERIFY_DIR/manifest.json") test -n "$DIGEST" curl -fsSL "$BASE/$ARTIFACT" -o "$VERIFY_DIR/$ARTIFACT" if command -v sha256sum >/dev/null 2>&1; then ACTUAL=$(sha256sum "$VERIFY_DIR/$ARTIFACT" | cut -d' ' -f1) else ACTUAL=$(shasum -a 256 "$VERIFY_DIR/$ARTIFACT" | cut -d' ' -f1) fi test "$ACTUAL" = "$DIGEST" tar -xzf "$VERIFY_DIR/$ARTIFACT" -C "$VERIFY_DIR" limilake "$VERIFY_DIR/limilake" --output json version ``` The reviewed installer performs these signature, archive-shape, digest, permission, and atomic replacement checks automatically. ### Installation troubleshooting - **`limilake: command not found`:** start a fresh shell, or add `$HOME/.local/bin` to `PATH`. Use `--install-dir` and `--no-modify-path` in a managed environment. - **Signature or digest failure:** stop. Do not bypass verification or reuse a partial download. Compare the public-key digest and artifact digest with the approved digest from an independently controlled channel. Authorized maintainers may also inspect the private GitHub Release audit. - **macOS Keychain or Linux Secret Service unavailable:** interactive logins use the OS credential store when it is unlocked. Headless Linux falls back to `~/.limilake/credentials.json` with mode `0600`; prefer `LIMILAKE_TOKEN` or `LIMILAKE_TOKEN_FILE` for stateless automation. - **Failed update:** run the existing binary's version command. If its identity is unchanged, retry the exact immutable version or restore a previously verified release. Do not delete the managed toolchain cache as a first step. ## 2. Authenticate ``` limilake auth login --host app.limilake.com --profile work ``` This runs a device-flow OAuth login and stores a tenant-bound access/refresh bundle in your OS keyring. Select another tenant only through the server-verified switch: ``` limilake tenant list --profile work limilake tenant use --profile work limilake context --profile work ``` Headless or CI? Import a personal access token through stdin (never an argv value): ``` printf '%s' "$LIMILAKE_PAT" | limilake auth login --token-stdin \ --host app.limilake.com --profile ci ``` For stateless environment authentication, set `LIMILAKE_TOKEN`, `LIMILAKE_HOST`, and `LIMILAKE_TENANT_ID` together. The Tenant ID is always required and must match the checkout's Tenant binding when you run from a linked checkout. PATs are tenant-bound and cannot switch. A token session, whether a PAT profile or `LIMILAKE_TOKEN`, authenticates as a shared user with no tenant-scoped identity. That is enough for everything in this guide except publishing: `limilake publish` and `limilake publication rollback` need an identity the platform can attribute the attempt to, so they are refused on a token session and have to be run from the device-flow profile above. Publishing explains why below. You can create a PAT from an OAuth profile: ``` limilake auth token create ci-token --days 90 --profile work ``` Check who you are at any time: ``` limilake auth status --profile work ``` ## 3. Find your data ``` limilake workspace list limilake lakehouse list --workspace my-workspace limilake table list --lakehouse my-lakehouse ``` `limilake lakehouse explain ` blends the live schema with the concept doc, so you can learn a lakehouse without leaving the terminal. ## 4. Query ``` limilake query "SELECT * FROM bronze.invoices LIMIT 10" --lakehouse my-lakehouse ``` Queries are read-only and bounded; they run through the platform's Lake Query API, so no local data engine is needed. ## 5. Author code Workspace bootstrap creates the repository. Clone and link it in one step: ``` limilake workspace clone my-workspace cd my-workspace ``` `workspace clone` runs ordinary Git against the canonical remote the platform advertises, configures Git credentials for that checkout, and links it. To do it by hand instead, clone the `clone_url` from `limilake --output json workspace show my-workspace` and run `limilake workspace link my-workspace` inside the checkout. Add resources by name. `add` only ever writes files in your checkout: ``` limilake workspace init # only for a checkout without limilake.toml limilake add function daily-report limilake add app sales-dashboard limilake add query active-customers ``` Edit `functions/daily-report/function.py` with your normal editor. The `add app` result names the frozen dependency-install command that must succeed before the local Vite loop can start; then refresh the generated client and start the loop: ``` pnpm install --frozen-lockfile --ignore-workspace limilake generate --check limilake dev ``` The loop binds `127.0.0.1` by default. For a browser on another machine over a trusted private network, name one specific reachable interface and use stable ports: ```text limilake dev --host 100.64.0.10 --port 4300 --app-port-base 4301 ``` The CLI prints the reachable App and Studio addresses. It refuses wildcard hosts such as `0.0.0.0`; anyone who can reach the chosen interface can reach the App previews and attempt trusted-host Local Studio requests. Open the exact printed `http://:/studio/` URL. The page automatically supplies its exact origin marker because Chrome may omit Fetch Metadata on private-network HTTP; there is no token or browser setting to copy. Other browser origins still cannot call Studio because the listener refuses their CORS preflight. If you do not want a remote listener, keep the default and forward the same stable ports over SSH: ```text limilake dev --port 4300 --app-port-base 4301 ssh -L 4300:127.0.0.1:4300 -L 4301:127.0.0.1:4301 user@dev-host ``` Local Function execution also needs the version-matched `limilake[serve]` Workspace runtime in `pyproject.toml` and `uv.lock`; `limilake add function` prints that dependency step and the required `limilake-run` entrypoint. Validate the complete Workspace with the same Build Engine used by production: ```text limilake build ``` With an ordinary signed standalone CLI, the first Build prints the exact managed Node, pnpm, CPython, and uv versions, immutable metadata URLs, transfer bounds, and the per-user cache destination. After the signed manifest and signature are verified, it prints the exact bundle and component sizes and digests before downloading the bundle. Later Builds verify and reuse that exact CLI-version cache. `limilake build --offline` performs no LimiLake-managed acquisition, leaves the managed cache unchanged, and requires dependency installation from the local pnpm store; it builds only when the compatible cache is already present and valid. Inside the hosted LimiLake Agent image, the same command uses the image-pinned standalone Build Engine and base-image tools instead of the managed cache. `--offline` is still forwarded to pnpm, but `toolchain doctor` and `repair` do not govern that override. Neither path is a network sandbox for Workspace code: local Vite configuration, plugins, scripts, and child processes still run as the trusted local user. The Workspace's `package.json`, `pyproject.toml`, and frozen lockfiles remain authoritative. ## Publish a Workspace Publishing activates one successful Build's artifacts for new traffic. The verb creates a Publication, and the noun inspects what it created: ```text limilake publish limilake publication show limilake publication rollback ``` Publish acts on the Workspace's server-side Draft Change, which is the `drafts/workspace` branch of the hosted repository, and never on your local checkout. Push the commits you want published to that branch first: ``` git push origin HEAD:drafts/workspace ``` Work that exists only in your local checkout is not part of the Publication, so a Publish run before that push activates whatever the Draft already held. `limilake publish` accepts the Workspace's Draft Change onto the protected branch, records a durable Publish attempt, and then follows that attempt until it finishes. Publishing is Workspace owner authority; a member who can draft and build is told to ask an owner rather than shown a raw error. The wait is bounded by `--timeout` (three minutes by default). The attempt is durable, so a poll that cannot reach the platform is retried inside that budget rather than ending the wait. A timeout exits non-zero without claiming the attempt failed: the platform keeps working on it. `--no-wait` reports the attempt's identity immediately instead, and `limilake publication show ` inspects it afterwards. That inspection is a single read: it prints the attempt as it stands at that moment and returns, so rerun it until the attempt reaches a terminal state, or drop `--no-wait` and let the command do the following for you. `--no-wait` skips the wait, not the verdict: an accepted attempt that is still running exits zero, while an attempt that was already decided against when it was recorded has its outcome printed in full and exits non-zero, so nothing reads `--no-wait` as a Publication that did not happen. Three outcomes are not a Publication, and in all three the previous Publication keeps serving: - **Conflict.** The protected branch advanced while the Draft was being published. The Workspace Agent is handed the reconciliation inside the Workspace, and the command reports the conflicting paths. Publish again once the Agent has saved the reconciled Draft; you are never asked to resolve a Git conflict by hand. - **Failure.** The attempt names its own cause, such as a Build that did not succeed, together with the attempt id you can inspect. - **Refusal.** Nothing to publish, a Publish or rollback already running, a protected branch that advanced while the Draft was being reconciled, a Workspace that is not active, a session that is no longer authenticated, a session the platform cannot attribute the attempt to, or a caller who is not an owner, each reported in its own words and in the action you ran. A Publish and a rollback share one lock per Workspace, so a request made while another one is running is answered with that running attempt, whatever kind it is and whoever started it. The command refuses that attempt by name rather than following it, so a rollback never reports someone else's Publish, and a Publish never reports another operator's in-flight attempt, as its own pointer move. Proving the attempt is yours needs an identity to compare it against, so both write commands need a session that carries one. Exactly one kind does: the device-flow OAuth profile from step 2, created by `limilake auth login` without `--token-stdin`. That login is what returns a tenant-scoped identity, and the attempt's `requested_by` is checked against it. The other three ways to authenticate do not carry one. A personal access token imported with `limilake auth login --token-stdin`, a stateless `LIMILAKE_TOKEN` environment session, and a sandbox session all authenticate as a shared user that the platform never hands back tenant-scoped, and the platform's reply to a Publish is the same document whether it created the attempt or handed back one that was already running. Those sessions are refused before anything is requested, with nothing published and nothing rolled back, rather than reporting an attempt that may be another operator's as their own. Run the command again from a device-flow profile, or ask someone who has one to run it. Reading is unaffected: `limilake publication show` works on every session, including the ones the write commands refuse, because a read claims nothing about who caused what it reads. `limilake publication rollback` returns to a retained Deployment Manifest. It is pointer movement over artifacts that were already built, so it never runs a Build. Read the manifest id from `limilake publication show`; a manifest that has left the retention window is refused rather than rebuilt. Every attempt field, including `build_id`, `deployment_manifest_id`, `publication_id`, `pointer_generation`, `conflict_paths`, and `failure_code`, is carried verbatim by `--output json` and `--output yaml`. A conflict that reported no paths keeps its empty `conflict_paths` list, and an attempt that never conflicted has none at all, so the two stay distinguishable in a script. There is no attempt history listing yet. `limilake publication show` reads the newest attempt, or one attempt by id, so record the ids you want to keep. ## Output formats Every command accepts a global `--output table|json|yaml` (default `table`), so the same commands serve humans and scripts. Use `limilake profile list|show|use|delete` for named environments. In parallel shells, set `LIMILAKE_PROFILE` or pass `--profile` so one shell never changes the other's effective tenant. `limilake --output json context` is the scripting contract for the resolved profile, identity, tenant, workspace, credential kind/capabilities, and expiry. --- # Querying data Data lives in **lakehouses** — server-side data containers, one per logical dataset within a workspace. A lakehouse holds **schemas** (commonly `bronze` / `silver` / `gold`, or custom), and schemas hold **tables** and **files**. Tables use **DuckLake**, DuckDB's native lakehouse format: the catalog lives in PostgreSQL and the data is Parquet in object storage. The catalog is the single source of truth — there is no repo `.lakehouse.yaml` to drift out of date. ## Discover ``` limilake lakehouse list limilake lakehouse schema my-lakehouse limilake table list --lakehouse my-lakehouse limilake lakehouse explain my-lakehouse ``` `explain` blends the live schema with this concept doc — the `kubectl explain` pattern. ## Read Ad-hoc SQL runs through the HTTP Lake Query API (read-only, bounded). No local data engine is required: ``` limilake query "SELECT customer, total FROM gold.sales ORDER BY total DESC LIMIT 20" \ --lakehouse my-lakehouse limilake table read gold.sales --lakehouse my-lakehouse --limit 50 ``` Attach more than one lakehouse with repeated `--lakehouse` flags; reference each by its catalog alias in the SQL. ## Provisioning Lakehouses are platform objects, created via the API, never authored as files: ``` limilake lakehouse create analytics --workspace my-workspace ``` ## Read-only by design Lake queries are read-only and capped at `--max-rows`. The real enforcement is server-side (scoped storage credentials, short-lived database roles, and token scopes), so a query can only touch the data your mounts and grants allow. --- # Functions A **function** lives at `functions//function.py` in a Workspace. Its capabilities are inferred statically from the source — there are no `.limilake/` sidecar files. A function can expose: - an **action** (`@fn.action`) — a callable operation, - **HTTP routes** — a public function data plane endpoint, - a **sync** (`@fn.sync`) — DuckLake-native ingestion, - a **schedule** (`schedule=` on the factory) — durable cron runs. A scheduled function whose body calls `agent.run()` is also how you run an **agent** on a schedule — there is no separate agent scheduler. See `limilake docs agents`. ## Anatomy ```python import limilake from limilake import logger fn = limilake.function( name="daily-report", access="private", run_as="caller", ) @fn.action def run(payload: dict[str, str]) -> dict[str, str]: rows = limilake.lake.read_table("bronze.invoices") logger.info(f"read {len(rows)} rows") return {"status": "ok", "requested_by": payload["requested_by"]} ``` An action or sync declares either no parameter, or one required `dict[str, T]` parameter. LimiLake delivers the complete run input JSON object as that one argument; object fields are not mapped onto Python parameters by name. Scalar parameters, multiple parameters, and parameter defaults are invalid. A zero-parameter handler ignores the input object. Managed delivery populates the governed runtime before your code runs. During `limilake dev`, a deliberate local action instead starts without platform authority: `logger` works immediately, while lake, AI, Connection, Document, Secret, and Agent capabilities bootstrap when your code first uses them. Keep governed module attributes such as `limilake.lake` inside the handler so the module can be imported before that capability is needed. Without an available CLI session, first use fails the local run with the actionable `capability_unavailable` error. There is no `def run(ctx):` wrapper and no manual import of credentials. Data access is scoped by the function's mounts, and providers are reached through `limilake.connections` (the egress gateway), never as raw secrets. Lazy local bootstrap changes when authority is acquired; the platform still authorizes every governed operation live. ## Access and execution identity Functions declare two separate choices: - `access="private" | "api_key" | "public"` controls who may invoke it. - `run_as="caller" | "assigned"` controls whose live tenant authority the runtime uses. The valid combinations are `private+caller`, `private+assigned`, `api_key+assigned`, and `public+assigned`. Private caller mode is the default. Caller-less access must use an assigned identity. The first publish of a new `run_as="assigned"` function assigns the linked Tenant Identity of the authenticated publisher. Git author and committer fields are never trusted for authority. Later code pushes preserve that assignment; an explicit reassignment or security-mode change revalidates through the same server policy. At runtime the identity, human membership, and workspace authority are checked live, so offboarding or lost authority fails closed until the function is reassigned. The older `auth=` spelling remains a deprecated compatibility alias. New code should use `access=` and `run_as=`. ## Lifecycle from the CLI Create the contract-shaped Function from the normal Workspace checkout, then edit its fixed entrypoint with your normal editor: ```bash limilake add function daily-report # edit functions/daily-report/function.py limilake function deploy functions/daily-report/function.py --workspace my-ws limilake function list # see deployed functions limilake function show functions/daily-report/function.py --workspace my-ws limilake function logs functions/daily-report/function.py --workspace my-ws ``` ## Running - **Live (default):** `limilake function invoke ` runs it server-side in a real sandbox — true production parity. - **Standalone local:** `limilake function invoke --local` and `limilake function serve` run the function in your workspace venv against live data and egress, for breakpoints and hot reload. These commands currently resolve the CLI session before starting the runtime. - **Workspace dev loop:** `limilake dev` runs deliberate local actions through the Workspace runtime without requiring a session for pure code or logging. Governed capabilities bootstrap only when used; HTTP Functions still require the CLI session because their long-lived serve runtime starts eagerly. Every deliberate-invocation surface uses that same object: `limilake function invoke --input '{"requested_by":"agent"}'`, the generated Workspace client, `@limilake/client`, and the documented run HTTP endpoint. Omitting input sends an empty object. Generated clients expose a no-argument method for a zero-parameter handler and a typed object argument for a `dict[str, T]` handler; their completed result type also follows the documented result value envelope. In every mode, governed data, AI, and egress calls hit the live platform and use live server authorization. Only pure deliberate actions in the Workspace dev loop can complete without contacting it. --- # Connections A **Connection** is a reusable routing and authentication handle granted to a workspace. Its credential values are stored separately in the encrypted credential vault. Function code and the CLI reach the provider through the egress gateway, which enforces the Connection definition and injects credentials server-side. The raw credential is never handed to the caller. ## In function code ```python from limilake import connections # GET/POST/request/paginate proxy through the egress gateway. resp = connections.get("stripe", "/v1/charges", params={"limit": 10}) data = resp.json() # paginate() follows the provider's paging for you. for page in connections.paginate("stripe", "/v1/customers"): ... ``` The ref you pass is the connection's **slug** (shown by `list_connections` and in the UI). It is matched against the grants your workspace holds, and the credential is injected at the gateway on each call. OAuth connections are refreshed at the gateway automatically. ## Revealing material Connections are call-through only: the gateway injects the credential and your code never holds it. To read raw credential material directly instead (a database password, a raw API key, anything non-HTTP), use a **Secret** and `secrets.reveal("ref")`, which returns the material — gated, and only for revealable refs. A connection that explicitly opted into reveal can also be read this way. ## Discover setup requirements The server owns one Setup Contract for every catalog provider and Custom Connection authentication profile. The portal, CLI, managed Agent, and external automation consume the same stable requirement keys and validation rules. ```bash limilake connection provider list limilake connection provider show stripe limilake --output json connection provider show stripe ``` The contract describes required fields, secret classification, choices, recommended and known OAuth scopes, possible next actions, and safe capabilities. It never contains submitted or stored values. ## Create a Connection An interactive human can submit known non-sensitive values and let the CLI hide any missing secret prompt: ```bash limilake connection create --provider wint --workspace finance \ --field username=11810 ``` Direct values remain supported, but command-line arguments may be retained in shell history or visible in process listings. Prefer structured standard input or a protected file for secrets: ```bash printf '%s' '{"username":"11810","password":"..."}' | \ limilake --output json connection create \ --provider wint --workspace finance --values-from - ``` This is also the external-Agent flow: inspect the JSON contract, ask the human only for missing requirements, submit values through standard input, and retain the returned stable Connection ref. Agents are not required to author provider YAML. Initial OAuth begins with the same `create` command. The CLI opens the browser unless `--no-browser` is set, then waits for the server-owned callback: ```bash limilake connection create --provider fortnox --workspace finance \ --scope invoice --scope supplier --timeout 5m limilake connection create --provider fortnox --workspace finance \ --no-browser --timeout 10m ``` Known scopes are recommendations, not a hard allowlist. Sanitized custom scope strings can be requested; the upstream provider makes the final decision. Replace a revoked grant or request changed scopes without changing the stable Connection ref: ```bash limilake connection reauthorize fortnox --workspace finance \ --scope invoice --scope custom.scope ``` Create a bounded Custom Connection without a YAML specification: ```bash printf '%s' '{"api_key":"..."}' | \ limilake connection create --custom --name partner-api --workspace finance \ --base-url https://api.partner.example/v1 \ --allowed-host api.partner.example \ --auth-method api_key --inject-header X-API-Key \ --values-from - ``` Custom routing still passes server validation: HTTPS only, explicit safe hosts, reviewed authentication profiles, and declarative header/query injection. Raw reveal-only material belongs under `limilake secret`, not a Custom Connection. ## Inspect, test, call, and delete ```bash limilake connection list limilake connection show stripe limilake connection test stripe limilake connection call stripe --method GET --path /v1/charges \ --query limit=10 limilake connection call stripe --method POST --path /v1/refunds \ --input @refund.json --output-file response.json limilake connection delete stripe --yes ``` `show` includes the definition, Setup Contract, lifecycle state, next action, safe capabilities, and granted OAuth scopes. `test` performs one read-only GET. `call` requires an explicit method and upstream-relative path; absolute URLs, credential headers, and oversized input/output are rejected. For each call the CLI exchanges its profile credential for a five-minute token bound to the verified tenant user, identity, and one workspace. That token is never printed or persisted. The egress gateway remains the enforcement and audit boundary for SSRF, credential injection, rate limits, and the selected Connection ref. ## Why no raw secrets in the sandbox The sandbox or short-lived CLI call token carries authority, not provider credentials. Injecting credentials server-side, per call, keeps secret material off untrusted machines and out of execution logs—the same boundary applies in a live sandbox, local function runtime, and external CLI. --- # LimiLake CLI primer You are operating a LimiLake workspace through the `limilake` CLI. Prefer it over guessing from training data — the platform's nouns and verbs are specific. When a detail is not covered here, run `limilake docs ` (offline) or `limilake explain` for live state. ## Mental model - **Workspace** -> **lakehouse** -> **schema** -> **table** / **file**. - Code is **functions** (`.py`) and **apps** (`.app.tsx`), authored as files in the workspace git repo. - Data containers (lakehouses) are **server-side platform objects** — discover and provision them via the API, never via a repo file. ## Act inline (don't ask) for - Discovery: `limilake workspace list`, `limilake lakehouse list`, `limilake table list --lakehouse `, `limilake lakehouse explain `. - Read-only queries: `limilake query "SELECT ..." --lakehouse ` and `limilake table read --lakehouse `. - Inspecting executions: `limilake execution list`, `limilake execution view `, `limilake execution logs `, `limilake function logs `. ## Confirm first for - Mutations: creating/deleting workspaces or lakehouses, deploying functions, granting credentials, revoking tokens. - Anything that moves production traffic: `limilake publish` activates a new Publication, so the workspace's live apps and functions start serving different artifacts, and `limilake publication rollback ` moves that same pointer back to an older retained Deployment Manifest. Both change what is serving right now, for everyone using the workspace's apps. Both also require a session with a tenant-scoped identity, which only a device-flow login (`limilake auth login` without `--token-stdin`) produces. A sandbox session, a PAT profile, and a `LIMILAKE_TOKEN` session are all refused, so ask the operator to run these rather than retrying or importing a token. Reading (`limilake publication show`) works from any session. ## Conventions - Every command takes a global `--output table|json|yaml`. Use `--output json` when you need to parse results. - Authentication is per-profile; `--profile ` overrides the active account. Inside a sandbox, credentials are injected automatically — no login needed. - Queries are read-only and bounded; the real access boundary is enforced server-side, so a query only ever sees data your grants allow. - In an ordinary signed standalone CLI install, inspect the exact managed Build toolchain with `limilake toolchain doctor`; use `limilake toolchain repair` only when its report names repair as the next action. - `limilake build` uses the same Build Engine as production. Signed standalone installs use the exact managed cache; the hosted LimiLake Agent image instead uses its image-pinned engine and base-image tools, so `toolchain doctor` and `repair` do not govern that override. Use `--offline` only when the active toolchain and required pnpm-store dependencies are already present; project manifests and frozen lockfiles remain authoritative. - `limilake publish` creates a **Publication**: it activates one successful Build's artifacts for new traffic. It publishes the Workspace's server-side Draft Change (the `drafts/workspace` branch), not your local checkout, so push local commits there first (`git push origin HEAD:drafts/workspace`). It waits for the attempt to finish by default and exits non-zero on a conflict, a failure, or an elapsed `--timeout`; the previous Publication keeps serving in every one of those cases. Inspect an attempt with `limilake publication show`, and return to a retained Deployment Manifest with `limilake publication rollback `. Both writes need an operator's device-flow session and are refused inside a sandbox, as "Confirm first for" says above; the inspect command is not. ## When to read more - How functions expose actions/HTTP/sync/schedules -> `limilake docs functions`. - Running an agent from code (`agent.run()`), resuming it by session, and scheduling it -> `limilake docs agents`. - Lakehouses, schemas, and SQL -> `limilake docs querying-data`. - Reaching external providers without raw secrets -> `limilake docs connections`. - First-time setup -> `limilake docs getting-started`. --- # Agents An **agent** is an autonomous LLM run that LimiLake executes server-side in a sandbox. The same primitive backs an interactive chat, a code-invoked run, and a scheduled run — there is one kind of thing, reached three ways. From code you invoke one with the `agent` capability, exactly like `lake` and `connections`: ```python from limilake import agent result = agent.run("Summarize yesterday's invoices and flag anything materially off") print(result.output) ``` `agent.run()` is a thin API client, not a local process spawn: it POSTs the prompt to the backend, which authorizes the caller, mints a narrowed token, launches the agent headless, blocks until it finishes, and returns the final assistant message. Because the agent runs server-side, the same call works from a laptop, from CI, and from inside a scheduled sandbox. ## Surface ```python agent.run(prompt, *, session=None, scope=None, timeout=None) -> AgentResult agent.start(prompt, *, session=None, scope=None, timeout=None) -> AgentHandle ``` - **`agent.run(prompt, ...)`** blocks until the agent reaches a terminal state and returns an `AgentResult`. This is the default for the "check, then invoke, then act" pattern below. - **`agent.start(prompt, ...)`** returns an `AgentHandle`; call `handle.result()` to get the `AgentResult`. The v1 server is synchronous, so the run is already complete when the handle is returned — `start` exists so calling code is forward-compatible with the deferred async launch/poll path. `AgentResult` is a frozen dataclass: - **`.output`** — the agent's final assistant message text. - **`.session`** — the durable agent id the run resolved to. Pass it back as `session=` to resume. - **`.status`** — the terminal run status: `completed`, `failed`, or `timeout`. - **`.usage`** — an opaque per-run usage dict (model, tokens) in v1; treat it as a pass-through. A failed invocation raises `AgentInvokeError`, which carries the backend HTTP `status_code` and a machine-readable `code` so you can branch on a permission denial versus a transient failure. ## Sessions and Tier-1 memory The `session` argument is the continuity handle. It is a **stable name**, resolved per workspace as a create-if-missing idempotency key: - The first call with a given name creates the durable agent and runs it fresh. - Later calls with the same name **resume the same agent and append the prompt** — "check again" reasons against everything that came before. ```python first = agent.run("Investigate this week's churn", session="churn-watch") again = agent.run("Now compare against last week", session="churn-watch") # resumes ``` The transcript lives on a persistent volume, so continuity survives the sandbox being torn down between runs. This is **Tier-1 memory**: the session *is* the memory, exactly like a human returning to a chat. There is no separate memory file to manage in v1. A `session` name is an idempotency key scoped to the workspace and the run-as owner — it is not a global handle, so two functions using the same name do not cross-resume into each other's transcript. Omit `session` for a fresh, ephemeral run with no continuity. ## Visibility and trigger types Every agent carries a **trigger** (how it was started) and an **`is_private`** (incognito) flag. - **Trigger** is how the agent was started: `interactive` (a chat), `on_demand` (run-now from a user or the CLI), or `code` (an `agent.run()` from a notebook or function body — including a scheduled function whose body calls `agent.run()`). All trigger types render in the same unified Agents list, filterable by type. - **`is_private`** (incognito) scopes an agent's *reads* to its creator. Agents are workspace-visible by default; an incognito agent is hidden from other members. Visibility only broadens **reads** — attaching to, driving, or mutating a running agent stays owner-only. Set incognito at create time in the UI, or toggle it per agent. Existing transcripts predating this feature are all private. ## Scope and what a code-invoked agent can do A code-invoked `agent.run()` does **not** run with your full rights. The backend mints a **narrowed** token, server-side, that is never trusted to the SDK: - It starts from a reduced default — read-only lake and workspace access (`workspace:read`, `workspace:lake:read`), the LLM and agent-tooling capabilities, and **egress proxy** for connection-proxied calls — plus the caller's granted connection refs. - It is intersected with the **authenticated caller's** actual grants, so an `agent.run()` from inside a sandbox can never escalate above what its caller holds. - It **hard-excludes** `egress:reveal`, `workspace:write`, and `workspace:lake:write` for any non-interactive run. The prompt is attacker-influenceable data, so a prompt-injected agent's worst case is read plus connection-proxied POST. The optional **`scope`** argument can only *narrow* the **data-plane** scopes further (intersect-only). A scope you pass can drop data authority from the default set; it can never add authority the caller does not already hold. The LLM and agent-tooling capabilities are always retained, so a narrowed run is still a runnable agent. ```python # Restrict this run's data plane to lake reads only — no egress at all. # (LLM and agent tooling stay available, so the agent can still run.) result = agent.run(prompt, scope=["workspace:lake:read"]) ``` An agent cannot invoke another agent: a call made from inside an agent sandbox is rejected server-side. ## Notifying via connections There is no separate notification API. An agent "notifies" by calling an HTTP **Connection** (a Slack webhook, an email provider, PagerDuty) through the egress proxy, with the credential injected server-side — the same path any provider call takes. To let an agent alert a channel, grant it the connection and mention it in the prompt: ```python agent.run( "If today's revenue dropped more than 20% vs the 7-day average, " "post a one-line summary to the 'slack-finance' connection.", ) ``` Dropping `egress:reveal` from the narrowed token does **not** break this: posting to a connection uses `egress:proxy` (server-side injection), not reveal. See `limilake docs connections`. ## Scheduling an agent Scheduling is **not** a new CLI verb. A scheduled agent is just a normal function with a `schedule=` whose body calls `agent.run()`. It rides the existing cron spine — the same leases, concurrency policy, and retry behavior every function gets. ```python from limilake import function, lake, agent fn = function(name="invoice-watch", schedule="0 7 * * *") # daily at 07:00 @fn.action def run() -> dict[str, object]: # Deterministic check FIRST: cheap, no LLM, fully programmable. n = lake.query("select count(*) n from bronze.invoices where date = current_date")[0]["n"] if n == 0: return {"skipped": "no new invoices"} # never spend an agent # Escalate to an agent only when warranted. r = agent.run( "Investigate today's invoices and report anything materially off. " "If something needs attention, post a summary via the 'slack-finance' connection.", session="invoice-watch", # stable name => same durable agent each fire ) return {"checked": n, "summary": r.output} ``` This **deterministic-check-then-invoke** shape is the intended pattern: do the cheap, fully-programmable check in plain code, and only pay for an agent when the check says it is worth it. The agent's session name keeps every fire continuous with the last. Notes for scheduled agents: - **Cadence floor.** A schedule that drives an agent must fire at most once every 15 minutes; daily or weekly is the economic sweet spot, since each fire is a sandbox plus a multi-turn LLM loop. - **Concurrency.** Set the function to forbid overlap (`max_concurrent_runs=1`) so a slow fire is skipped, not stacked. - **Dedup.** Do your own "did I already handle this?" check in code (for example a high-water-mark in a lake table). There is no dedicated cursor subsystem in v1. See `limilake docs functions` for the full function/scheduling surface. ## Where it runs `pi` (the agent runtime) lives only server-side. `agent.run()` carries no local LLM — it authorizes against the backend, which launches the agent headless in a managed sandbox and captures the result. Inside a managed sandbox the token and workspace are injected automatically; from a laptop or CI, set `LIMILAKE_TOKEN` (or `LIMILAKE_TOKEN_FILE`) and `LIMILAKE_WORKSPACE_ID`.