Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable changes to LodeDB are documented here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [1.4.0] - 2026-07-18

### Added

Expand All @@ -26,16 +26,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `LodeDB.cloud("user-42")` opens a managed-cloud store through the same
class as a local path, joining the `open_readonly`/`open_vector_store`
alternate-constructor family: a bare store id resolves its
org/environment from the credential (`token=`,
`ORECLOUD_TOKEN`/`ORECLOUD_HOST`, or `lodedb cloud login`), and the
`"org/environment/store"` triple and full `orecloud://` URLs are accepted
too. The call returns the cloud store handle (same
org/environment from the credential (`token=`, `ORECLOUD_TOKEN`, or
`lodedb cloud login`), and the `"org/environment/store"` triple and
full `orecloud://` URLs are accepted too. The call returns the cloud store handle (same
`add`/`search`/`get`/`remove` verbs over HTTPS). For config-driven code,
the plain constructor also dispatches the explicit URL form —
`LodeDB("orecloud://org/environment/store")` — through the same funnel.
Local-only construction options (`model=`, `read_only=`, ...) are rejected
on cloud targets with a targeted error, and a cloud target without the
`[cloud]` extra's dependencies raises the install hint.
- The client and CLI default to the hosted control plane
(`https://api.egoistmachines.com`), so nobody has to know the URL:
`lodedb cloud login` needs no flags, and `Client(token=...)` or
`ORECLOUD_TOKEN` alone is enough for CI. `--host`/`ORECLOUD_HOST`
remain the override for staging and self-hosted control planes; a
host override without a matching token still fails closed.

## [1.3.2] - 2026-07-16

Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ members = [
resolver = "2"

[workspace.package]
version = "1.3.2"
version = "1.4.0"
edition = "2021"
rust-version = "1.88"
license = "Apache-2.0"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -633,8 +633,8 @@ db.search("fox", k=5)
```

A bare store id is enough — the org/environment half resolves from the
credential (`token=`, the `ORECLOUD_TOKEN`/`ORECLOUD_HOST` environment pair,
or `lodedb cloud login`); pass the full `"org/environment/store"` triple in
credential (`token=`, the `ORECLOUD_TOKEN` environment variable, or
`lodedb cloud login`); pass the full `"org/environment/store"` triple in
cross-environment scripts. For config-driven code, where one string field (an
env var, a YAML value) must express either a local path or a cloud store, the
plain constructor accepts the explicit URL form —
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "maturin"
[project]
name = "lodedb"
# Static (maturin reads it here); keep in sync with src/lodedb/__init__.py __version__.
version = "1.3.2"
version = "1.4.0"
description = "Local-first, privacy-by-default embedded vector database. Your data never leaves your machine."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion src/lodedb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from lodedb.local.cli import app, main

# Keep in sync with `version` in pyproject.toml (the release workflow asserts they match).
__version__ = "1.3.2"
__version__ = "1.4.0"

__all__ = [
"LOCAL_MODEL_PRESETS",
Expand Down
9 changes: 8 additions & 1 deletion src/lodedb/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
import inspect
from typing import Any

# The hosted control plane every credential path defaults to (stdlib-only
# module, so this eager import keeps the plain-import boundary intact);
# --host / ORECLOUD_HOST / host= override it.
from lodedb.cloud._config import DEFAULT_HOST as DEFAULT_HOST

# Managed-cloud target scheme recognized by the LodeDB(...) config-string
# dispatch. LodeDB.cloud() additionally accepts scheme-less short forms
# ("user-42", "org/environment/store"); the plain constructor must not,
Expand Down Expand Up @@ -50,7 +55,9 @@
"SyncConflictError": "transfer",
}

__all__ = sorted({"CLOUD_TARGET_SCHEME", "open_cloud_target", *_NATIVE_EXPORTS, *_HTTP_EXPORTS})
__all__ = sorted(
{"CLOUD_TARGET_SCHEME", "DEFAULT_HOST", "open_cloud_target", *_NATIVE_EXPORTS, *_HTTP_EXPORTS}
)


def open_cloud_target(target: str, options: dict[str, Any]) -> Any:
Expand Down
5 changes: 3 additions & 2 deletions src/lodedb/cloud/_agents_scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,9 @@ def agents_md_section(host: str, org: str, environment: str, store: str) -> str:
lodedb cloud store list --environment {environment} # registered stores
```

- Credentials: set both `ORECLOUD_HOST` and `ORECLOUD_TOKEN`, or run
`lodedb cloud login --host {host}` once. Never commit tokens.
- Credentials: set `ORECLOUD_TOKEN` (plus `ORECLOUD_HOST` when the control
plane is not the hosted default), or run `lodedb cloud login --host {host}`
once. Never commit tokens.
- The CLI prints JSON when piped; errors carry a `hint:` line and per-class
exit codes (3 auth, 4 not found, 5 refused, 6 transient).
- SDK writes are asynchronous: `add_many` returns the document ids once the
Expand Down
36 changes: 24 additions & 12 deletions src/lodedb/cloud/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
control-plane host and the personal access token from `lodedb cloud login`.
A plain file (not the OS keychain) is deliberate for now: it works headless
— CI boxes, containers, SSH sessions — with the same permission story as
`~/.aws/credentials`. ORECLOUD_TOKEN / ORECLOUD_HOST env vars override the
file entirely, so ephemeral environments never need to write it.
`~/.aws/credentials`. ORECLOUD_TOKEN overrides the file entirely, so
ephemeral environments never need to write it; the host defaults to the
hosted control plane, with ORECLOUD_HOST (or `--host`) overriding it for
staging and self-hosted deployments.
"""

from __future__ import annotations
Expand All @@ -16,6 +18,11 @@
from dataclasses import dataclass
from pathlib import Path

# The hosted control plane. Baked in so nobody has to know the URL (the same
# way `gh` knows github.com); `--host` / ORECLOUD_HOST remain the override
# for staging and self-hosted control planes.
DEFAULT_HOST = "https://api.egoistmachines.com"


def config_dir() -> Path:
# Computed per call, not at import: ORECLOUD_CONFIG_DIR must work when
Expand All @@ -42,17 +49,22 @@ class CredentialsError(RuntimeError):
def load_credentials() -> Credentials | None:
env_token = os.environ.get("ORECLOUD_TOKEN")
env_host = os.environ.get("ORECLOUD_HOST")
if env_token and env_host:
return Credentials(host=env_host.rstrip("/"), token=env_token, source="env")
if env_token or env_host:
# Half an override must fail closed: silently mixing an env token
# with a file host (or vice versa) would aim mutating commands at the
# wrong account or control plane.
missing = "ORECLOUD_HOST" if env_token else "ORECLOUD_TOKEN"
present = "ORECLOUD_TOKEN" if env_token else "ORECLOUD_HOST"
if env_token:
# An env token never combines with the file's host: falling back to
# the file could aim a CI job's mutating commands at whatever control
# plane a developer last logged in to. Without ORECLOUD_HOST the
# token targets the hosted default.
return Credentials(
host=(env_host or DEFAULT_HOST).rstrip("/"), token=env_token, source="env"
)
if env_host:
# A host override with no matching token must fail closed: silently
# pairing it with the file's token would aim that stored credential
# at a control plane the login never targeted.
raise CredentialsError(
f"{present} is set but {missing} is not — set both to use environment "
"credentials, or unset both to use the stored credentials file"
"ORECLOUD_HOST is set but ORECLOUD_TOKEN is not — set ORECLOUD_TOKEN "
"to use environment credentials, or unset ORECLOUD_HOST to use the "
"stored credentials file"
)
try:
raw = json.loads(credentials_file().read_text())
Expand Down
38 changes: 20 additions & 18 deletions src/lodedb/cloud/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,8 @@ def _classify(error: CloudError) -> tuple[int, str | None]:
status = error.status_code
if status == 401:
return EXIT_AUTH, (
"the credential was rejected — run `lodedb cloud login --host <control-plane-url>`, "
"or set both ORECLOUD_HOST and ORECLOUD_TOKEN"
"the credential was rejected — run `lodedb cloud login` "
"or set ORECLOUD_TOKEN"
)
if status == 403:
return EXIT_AUTH, (
Expand Down Expand Up @@ -419,7 +419,7 @@ def _load_credentials() -> "_config.Credentials | None":
def _client() -> CloudClient:
creds = _load_credentials()
if creds is None:
raise _fail("not logged in — run `lodedb cloud login --host <control-plane-url>`")
raise _fail("not logged in — run `lodedb cloud login`")
return CloudClient(creds.host, creds.token)


Expand Down Expand Up @@ -505,7 +505,7 @@ def _managed(local_dir: str, remote: str) -> tuple[CloudClient, ManagedRemote, s
)
creds = _load_credentials()
if creds is None:
raise _fail("not logged in — run `lodedb cloud login --host <control-plane-url>`")
raise _fail("not logged in — run `lodedb cloud login`")
if creds.host.rstrip("/") != config.host:
raise _fail(
f"this directory is linked to {config.host} but you are logged in to "
Expand All @@ -518,15 +518,19 @@ def _managed(local_dir: str, remote: str) -> tuple[CloudClient, ManagedRemote, s
return None
creds = _load_credentials()
if creds is None:
raise _fail("not logged in — run `lodedb cloud login --host <control-plane-url>`")
raise _fail("not logged in — run `lodedb cloud login`")
return CloudClient(creds.host, creds.token), target, creds.host


@app.command()
def login(
host: Annotated[
str | None,
typer.Option("--host", help="Control-plane URL, e.g. https://console.example.com."),
typer.Option(
"--host",
help="Control-plane URL for staging or self-hosted deployments "
f"(default: the stored login's host, else {_config.DEFAULT_HOST}).",
),
] = None,
token: Annotated[
str | None,
Expand All @@ -551,9 +555,10 @@ def login(
"""
creds = _load_credentials()
if host is None:
host = creds.host if creds else None
if host is None:
raise _fail("--host is required for the first login")
# Re-login sticks to the control plane already logged in to (switching
# planes is always an explicit --host); first login targets the hosted
# default.
host = creds.host if creds else _config.DEFAULT_HOST

if token is None:
token = _browser_login(host, open_browser=not no_browser)
Expand Down Expand Up @@ -605,11 +610,7 @@ def _ensure_logged_in(host: str | None) -> "_config.Credentials":
)
return creds
if host is None:
raise _fail(
"not logged in — pass --host to log in as part of this command, "
"or run `lodedb cloud login --host <control-plane-url>` first",
code=EXIT_USAGE,
)
host = _config.DEFAULT_HOST
token = _browser_login(host)
with CloudClient(host, token) as client:
me = _cloud(client.me)
Expand Down Expand Up @@ -722,8 +723,9 @@ def init(
str | None,
typer.Option(
"--host",
help="Control-plane URL — when no credential is stored, init logs "
"in first (browser approval, the one human step).",
help="Control-plane URL for staging or self-hosted deployments "
f"(default: {_config.DEFAULT_HOST}) — when no credential is "
"stored, init logs in first (browser approval, the one human step).",
),
] = None,
agents: Annotated[
Expand Down Expand Up @@ -821,7 +823,7 @@ def auth_print_headers() -> None:
creds = _load_credentials()
if creds is None:
raise _fail(
"not logged in — run `lodedb cloud login --host <control-plane-url>`",
"not logged in — run `lodedb cloud login`",
code=EXIT_AUTH,
)
typer.echo(json.dumps({"Authorization": f"Bearer {creds.token}"}))
Expand Down Expand Up @@ -946,7 +948,7 @@ def tokens_mint(
org/environment; personal tokens take neither flag."""
creds = _load_credentials()
if creds is None:
raise _fail("not logged in — run `lodedb cloud login --host <control-plane-url>`")
raise _fail("not logged in — run `lodedb cloud login`")
with CloudClient(creds.host, creds.token) as client:
if kind != "personal" and (org is None or environment is None):
org, environment = _tenancy(client, org, environment)
Expand Down
20 changes: 8 additions & 12 deletions src/lodedb/cloud/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
server rejecting a mismatched binding at the first request.

Credentials resolve like the CLI's: explicit ``token=``/``host=`` arguments,
then the ``ORECLOUD_TOKEN``/``ORECLOUD_HOST`` environment pair, then the
``lodedb cloud login`` credentials file.
then the ``ORECLOUD_TOKEN`` environment variable, then the ``lodedb cloud
login`` credentials file. The host defaults to the hosted control plane;
``host=`` / ``ORECLOUD_HOST`` override it for self-hosted deployments.

Every control-plane verb the SDK speaks hangs off this object with the
tenancy already applied; per-store reads and writes live on the
Expand All @@ -41,28 +42,23 @@


def resolve_credentials(token: str | None, host: str | None) -> tuple[str, str]:
"""(host, token) from explicit arguments, the environment pair, or the
`lodedb cloud login` file — raising a CloudError naming the fix when either
half is missing."""
"""(host, token) from explicit arguments, the environment, or the
`lodedb cloud login` file. The host falls back to the hosted control
plane (`DEFAULT_HOST`), so a bare token is always enough; only a missing
token raises, with a CloudError naming the fix."""
if token is None or host is None:
stored = _config.load_credentials()
if token is None:
token = stored.token if stored else None
if host is None:
host = stored.host if stored else None
if not host:
raise CloudError(
401,
"no control-plane host configured — pass host=, set ORECLOUD_HOST, "
"or run `lodedb cloud login`",
)
if not token:
raise CloudError(
401,
"no credential configured — pass token=, set ORECLOUD_TOKEN, or run "
"`lodedb cloud login`",
)
return host, token
return (host or _config.DEFAULT_HOST).rstrip("/"), token


def resolve_tenancy(
Expand Down
14 changes: 8 additions & 6 deletions src/lodedb/cloud/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
`recall`, `context_block`, `browse`, and `delete_memories`.

Credentials resolve like the CLI's: explicit ``token=``/``host=`` arguments
win, then the ``ORECLOUD_TOKEN``/``ORECLOUD_HOST`` environment pair, then the
``lodedb cloud login`` credentials file. Server-side, queries are embedded with
the same preset that indexed the data, so scores are the engine's own.
win, then the ``ORECLOUD_TOKEN`` environment variable, then the ``lodedb
cloud login`` credentials file; the host defaults to the hosted control
plane. Server-side, queries are embedded with the same preset that indexed
the data, so scores are the engine's own.

Connecting pre-warms the store on the serving tier by default (the first
query then skips the hydration cold start); pass ``warm=False`` to skip.
Expand Down Expand Up @@ -710,9 +711,10 @@ def connect(
resolves from the credential via `resolve_tenancy`, exactly like
`Client().store()`), `"org/environment/store"`, or an `orecloud://` URL
of either (the URL form also allows `org/environment`, defaulting the
store). Credentials: explicit arguments, else the
`ORECLOUD_TOKEN`/`ORECLOUD_HOST` environment pair, else the credentials
file `lodedb cloud login` wrote. `warm=True` (default) asks the serving tier
store). Credentials: explicit arguments, else the `ORECLOUD_TOKEN`
environment variable, else the credentials file `lodedb cloud login`
wrote (the host defaults to the hosted control plane). `warm=True`
(default) asks the serving tier
to hydrate and open the store now, so the first query is warm; it also
verifies the target exists and the credential can read it. `key` names
the index key when the store holds more than one (rare — a pushed LodeDB
Expand Down
Loading
Loading