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
18 changes: 15 additions & 3 deletions deploy/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,30 @@
# otherwise each cold start re-births a fresh demo soul.
set -e

# Match the app's log discipline (src/log.ts): on Cloud Run (K_SERVICE) emit
# one-line JSON so Cloud Logging lifts the severity instead of filing these as
# unclassified; plain text anywhere else. Messages here are fixed strings with
# no quotes or backslashes, so no JSON escaping is needed.
log_line() {
if [ -n "$K_SERVICE" ]; then
printf '{"severity":"%s","message":"%s"}\n' "$1" "$2"
else
echo "$2"
fi
}

export LISA_HOME="${LISA_HOME:-/data}"
mkdir -p "$LISA_HOME"

# Born? isBorn() resolves true once the soul seed exists under $LISA_HOME.
if node -e "import('./dist/soul/store.js').then(m=>m.isBorn()).then(b=>process.exit(b?0:1)).catch(e=>{console.error(e);process.exit(1)})"; then
echo "[cloud] soul already present — skipping birth"
log_line INFO "[cloud] soul already present — skipping birth"
else
echo "[cloud] birthing the demo soul (model ${LISA_MODEL:-default})…"
log_line INFO "[cloud] birthing the demo soul (model ${LISA_MODEL:-default})…"
# `lisa birth` validates that a provider key for the model is configured (any of
# ANTHROPIC_API_KEY / ZHIPU_API_KEY / OPENAI_API_KEY / … per the model). If none
# is set it exits non-zero and the first web visitor gets the birth ritual.
node dist/cli.js birth || echo "[cloud] birth skipped/failed — first visitor will see the birth ritual"
node dist/cli.js birth || log_line WARNING "[cloud] birth skipped/failed — first visitor will see the birth ritual"
fi

exec node dist/cli.js serve --web --port "${PORT:-8080}" --host 0.0.0.0
5 changes: 3 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { dirname, resolve as resolvePath } from "node:path";
// in ~/.lisa/config.env.
import { configureProxyFromEnv } from "./proxy-bootstrap.js";
configureProxyFromEnv({ log: (m) => console.error(m) });
import { logInfo } from "./log.js";
import { runAgent } from "./agent.js";
import { buildApprovalCallback, DEFAULT_MUTATING_TOOLS, DEFAULT_MUTATING_ACTIONS } from "./approval.js";
import { CONFIG_ENV_PATH, loadConfigEnv } from "./env.js";
Expand Down Expand Up @@ -570,9 +571,9 @@ async function main(): Promise<void> {
// Print the real bind address — this used to claim "localhost" while
// Node was actually listening on every interface.
const display = isLoopbackAddress(args.host) ? "localhost" : args.host;
console.error(`Lisa web UI listening on http://${display}:${args.port} (bound to ${args.host})`);
logInfo(`Lisa web UI listening on http://${display}:${args.port} (bound to ${args.host})`);
if (!isLoopbackAddress(args.host)) {
console.error(
logInfo(
`[web] non-loopback bind: requests from other machines must present LISA_WEB_TOKEN ` +
`(open http://${args.host}:${args.port}/?token=<value> once per device)`,
);
Expand Down
11 changes: 7 additions & 4 deletions src/log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ test("redactId keeps a prefix+suffix for correlation, never the middle", () => {
assert.equal(redactId(""), "");
});

test("redactEmail keeps first char + domain only", () => {
assert.equal(redactEmail("alice@example.com"), "a***@example.com");
assert.equal(redactEmail("not-an-address"), "…");
assert.equal(redactEmail("@nouser.com"), "…");
test("redactEmail drops the identifying local part, keeps the domain", () => {
assert.equal(redactEmail("alice.smith@example.com"), "al***@example.com");
assert.equal(redactEmail("a@b.co"), "a***@b.co");
// Anything that isn't an address must not fall through as-is.
assert.equal(redactEmail("not-an-address"), "***");
assert.equal(redactEmail("@nouser.com"), "***");
assert.equal(redactEmail("trailing@"), "***");
});
14 changes: 9 additions & 5 deletions src/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,13 @@ export function redactId(id: string): string {
return `${id.slice(0, 4)}…${id.slice(-4)}`;
}

/** `alice@example.com` → `a***@example.com`; a non-address becomes `…`. */
export function redactEmail(email: string): string {
const at = email.indexOf("@");
if (at <= 0) return "…";
return `${email[0]}***@${email.slice(at + 1)}`;
/**
* `alice.smith@example.com` → `al***@example.com`. The local part is the
* identifying half, so it goes; the domain stays whole because that's what you
* group by when delivery breaks. Anything that isn't an address becomes `***`.
*/
export function redactEmail(addr: string): string {
const at = addr.lastIndexOf("@");
if (at <= 0 || at === addr.length - 1) return "***";
return `${addr.slice(0, Math.min(2, at))}***@${addr.slice(at + 1)}`;
}
3 changes: 2 additions & 1 deletion src/soul/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
import path from "node:path";
import { pathExists } from "../fs-utils.js";
import { isCloud } from "../edition.js";
import { logWarn } from "../log.js";
import { withFileLock } from "./lock.js";
import { soulDir } from "./paths.js";

Expand Down Expand Up @@ -137,7 +138,7 @@ async function runGit(args: string[]): Promise<GitResult> {
*/
export async function initSoulRepo(): Promise<void> {
if (!(await checkGitAvailable())) {
console.warn("[soul-git] git not available; soul history disabled");
logWarn("[soul-git] git not available; soul history disabled");
return;
}
if (!(await pathExists(soulDir()))) return;
Expand Down
23 changes: 9 additions & 14 deletions src/web/mailer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
*
* Env: RESEND_API_KEY, LISA_MAIL_FROM (default "LISA <no-reply@mail.meetlisa.ai>").
*/
import { logInfo, logWarn, logError, redactEmail } from "../log.js";

export interface MailResult {
sent: boolean;
Expand Down Expand Up @@ -66,16 +67,10 @@ export function mailerConfig(env: Record<string, string | undefined> = process.e

// ── log hygiene ─────────────────────────────────────────────────────────────

/**
* `alice.smith@example.com` → `al***@example.com`. The local part is the
* identifying half, so it goes; the domain stays whole because that's what you
* group by when delivery breaks.
*/
export function redactEmail(addr: string): string {
const at = addr.lastIndexOf("@");
if (at <= 0 || at === addr.length - 1) return "***";
return `${addr.slice(0, Math.min(2, at))}***@${addr.slice(at + 1)}`;
}
// The canonical implementation lives in ../log.js beside the other log-line
// redactors; re-exported here because callers of this module reach for it as
// part of the mail surface.
export { redactEmail };

// ── HTML templating (hand-rolled: no MJML, no react-email, no deps) ─────────
// Email HTML is 1998 HTML — tables, inline styles, no flexbox, no <style> that
Expand Down Expand Up @@ -255,7 +250,7 @@ async function deliver(
if (!cfg.apiKey) {
// The one place a live credential is logged: no key means no delivery, so
// the alternative is a sign-in nobody can complete.
console.error(`[mail] outcome=skipped_no_key kind=${kind} to=${who} — ${fallback}`);
logWarn(`[mail] outcome=skipped_no_key kind=${kind} to=${who} — ${fallback}`);
return { sent: false, detail: "no_api_key" };
}
try {
Expand All @@ -278,17 +273,17 @@ async function deliver(
// limit) that returns a perfectly well-formed response — it must be
// logged as loudly as a thrown network error, or it vanishes.
const body = await res.text().catch(() => "");
console.error(
logError(
`[mail] outcome=send_failed kind=${kind} to=${who} status=${res.status} — ${body.slice(0, 200)}`,
);
return { sent: false, detail: `http_${res.status}` };
}
const parsed = (await res.json().catch(() => ({}))) as { id?: string };
const id = parsed.id ?? "sent";
console.error(`[mail] outcome=success kind=${kind} to=${who} id=${id}`);
logInfo(`[mail] outcome=success kind=${kind} to=${who} id=${id}`);
return { sent: true, detail: id };
} catch (e) {
console.error(`[mail] outcome=send_failed kind=${kind} to=${who} — ${(e as Error).message}`);
logError(`[mail] outcome=send_failed kind=${kind} to=${who} — ${(e as Error).message}`);
return { sent: false, detail: "network_error" };
}
}