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": {