From 8f459c7023c42c70b9bff93f3f15c828c2db5f83 Mon Sep 17 00:00:00 2001 From: jochens Date: Wed, 30 Sep 2026 12:22:12 +0200 Subject: [PATCH] letta: placed state, a route, accepted keys, and a runtime that can log in The module named /var/lib/letta, a layout no definition may carry (ADR 0112); state is now a placed directory holding the bindings and secrets. letta had no route, while the server it replaces is reached by its public name (a workflow calls it there). It now contributes one for its `web` endpoint; reach is the assignment's. The server needs an OpenAI key for agents on OpenAI models, and nothing gave it one: `openai-api-key` is an own-secret, accepted from the operator (a key someone chose, not one the mesh can mint). The server-password is accepted the same way where a server already has clients. Both, and the database password inside LETTA_PG_URI, stay in the server's environment: letta 0.6.x reads settings from the environment only, and its startup.sh starts an embedded PostgreSQL unless LETTA_PG_URI is set - the declared reason now says so. The runtime's tools never authenticated: the client sent only a Bearer token, and 0.6.x's --secure mode checks X-BARE-PASSWORD ("password ") and answers 401 otherwise. The client now sends both. Its password comes from the runtime config file (the key client.ts reads first) instead of an env-file, so the runtime container no longer carries a secret in its environment. Image: the same 0.6.8 image, now pinned by the index digest ace runs rather than its amd64 manifest. Verified: catalogue tests with MESH_CATALOGUE set; tsc -p tsconfig.json in the mesh-tools build image. Throwaway containers: a fresh 0.6.8 with its embedded PG and two blocks made through the API; stopped, copied, dumped from the copy; restored (schema letta + vector pre-made by the superuser, --no-owner --role, search_path set on the database as the original had it) into a grant-shaped database on the postgres module's pgvector image (PG17); started in this shape: alembic finds nothing to do, both blocks are there, a wrong password is refused with 401, and the patched client lists agents. Test containers and data removed. --- modules/letta/client.ts | 12 ++++++---- modules/letta/module.json | 46 ++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/modules/letta/client.ts b/modules/letta/client.ts index 6e194db..4216e60 100644 --- a/modules/letta/client.ts +++ b/modules/letta/client.ts @@ -1,10 +1,10 @@ // The Letta API client — letta's own code, living in the module (novox/hq ADR 0039). Its tools // import it; nothing outside letta does. // -// Letta authenticates with a single server password, presented as a Bearer token. That password is -// a mesh own-secret, minted once and handed to both the server (LETTA_SERVER_PASSWORD) and this -// client (MESH_LETTA_PASSWORD) — so the module's tools are live without anything configured by hand. -// The runtime config file may still override the URL or password. +// Letta authenticates with a single server password. That password is a mesh own-secret handed to +// both the server (LETTA_SERVER_PASSWORD) and this client, through the runtime config file the mesh +// mounts (its `password` key) — so the module's tools are live without anything configured by hand. +// Where a server already has clients, the password is accepted rather than minted. import { readFileSync } from "node:fs"; @@ -58,6 +58,10 @@ export class LettaClient { ...options, headers: { "Content-Type": "application/json", + // The server's --secure mode checks X-BARE-PASSWORD ("password ") and answers a Bearer + // token alone with 401 (letta/server/rest_api/app.py, 0.6.x). Both are sent: Bearer is what + // later servers read. + "X-BARE-PASSWORD": `password ${this.password}`, Authorization: `Bearer ${this.password}`, ...(options.headers as Record | undefined), }, diff --git a/modules/letta/module.json b/modules/letta/module.json index 4cd08c0..2fa834d 100644 --- a/modules/letta/module.json +++ b/modules/letta/module.json @@ -5,21 +5,28 @@ "container-runtime" ], "requires": [ - "postgres-database" + "postgres-database", + "route" ], "contributes": { "postgres-database": { "name": "letta" + }, + "route": { + "label": "letta", + "endpoint": "web" } }, "binds": { - "postgres-database": "/var/lib/letta/database.json" + "postgres-database": "${dir:state}/database.json", + "route": "${dir:state}/route.json" }, "secrets": { - "postgres-database": "/var/lib/letta/database.secret" + "postgres-database": "${dir:state}/database.secret" }, "own-secrets": { - "server-password": "/var/lib/letta/server-password.secret", + "server-password": "${dir:state}/server-password.secret", + "openai-api-key": "${dir:state}/openai-api-key.secret", "broker": "/var/lib/mesh/letta/broker" }, "listens": [ @@ -28,7 +35,7 @@ "port": 8283, "protocol": "tcp", "from": "mesh", - "why": "the Letta agent server REST API and web UI; a public name is a route grant later" + "why": "the Letta agent server REST API and web UI, password-protected (--secure); a public name is the route's" } ], "resources": [ @@ -41,15 +48,15 @@ { "id": "state", "type": "directory", - "path": "/var/lib/letta", - "mode": "0700" + "mode": "0700", + "place": "." }, { "id": "server-env", "type": "file", - "path": "/var/lib/letta/server.env", + "path": "${dir:state}/server.env", "mode": "0600", - "content": "LETTA_PG_URI=postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\nLETTA_SERVER_PASSWORD=${secret:server-password}\nSECURE=true\nTZ=Europe/Brussels\n" + "content": "LETTA_PG_URI=postgresql://${bound:postgres-database:as}:${secret:postgres-database}@${bound:postgres-database:at}:${bound:postgres-database:port}/${bound:postgres-database:as}\nLETTA_SERVER_PASSWORD=${secret:server-password}\nOPENAI_API_KEY=${secret:openai-api-key}\nSECURE=true\nTZ=Europe/Brussels\n" }, { "id": "net", @@ -60,31 +67,24 @@ "id": "server", "type": "container", "name": "letta", - "image": "letta/letta@sha256:1d2e0692514287c5ed1a483e14e16ed945f8632d315539f5e66373bb7d7c471b", + "image": "letta/letta@sha256:bfd1e49ce45b9a208c941e832c1d1d194017ff210a3784b0ca6c323aed767a29", "network": "letta", "env-file": [ - "/var/lib/letta/server.env" + "${dir:state}/server.env" ], "ports": [ "8283" ], - "secrets-in-environment": "the letta image is env-driven and its file-source support could not be verified; the mesh runtime can take its password from config.json (client.ts) \u2014 not yet converted" + "secrets-in-environment": "letta 0.6.x reads its settings from the environment only (pydantic settings, no secrets_dir or _FILE twin), and its startup.sh starts an embedded PostgreSQL unless LETTA_PG_URI is set - so the database password travels inside that URI (startup.sh also echoes it to the log); LETTA_SERVER_PASSWORD and OPENAI_API_KEY have no file source either" }, { "id": "runtime-config", "type": "file", "path": "/var/lib/mesh/letta/config.json", "mode": "0600", - "content": "{}\n", + "content": "{\n \"password\": \"${secret:server-password}\"\n}\n", "merge": "json" }, - { - "id": "runtime-env", - "type": "file", - "path": "/var/lib/letta/runtime.env", - "mode": "0600", - "content": "MESH_LETTA_PASSWORD=${secret:server-password}\n" - }, { "id": "runtime", "type": "container", @@ -99,14 +99,10 @@ "MESH_LETTA_URL": "http://letta:8283", "MESH_LETTA_CONFIG_FILE": "/run/config/config.json" }, - "env-file": [ - "/var/lib/letta/runtime.env" - ], "restart-on": [ "runtime-config" ], - "artifact": "runtime", - "secrets-in-environment": "the letta image is env-driven and its file-source support could not be verified; the mesh runtime can take its password from config.json (client.ts) \u2014 not yet converted" + "artifact": "runtime" } ], "build": {