Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/baiganio_aperio.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
889 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "baiganio/aperio", "files": {"k8s/configmap.yaml": {"2b2a200280e92e939fc11bf60927fdf887f58cf7": {"t": 1785745201.2082624, "n": 1705, "h": "bae085e234f18568", "c": "---\n# ConfigMap — first-boot configuration for Aperio.\n#\n# EDIT THIS FILE BEFORE FIRST DEPLOY on your Pi. Uncomment the lines for\n# the provider you want and set the values. If everything stays commented,\n# Aperio uses its built-in defaults (llamacpp + transformers), then you can\n# change them via the Web UI → Settings afterward.\n#\n# Once you save settings in the Web UI, they're stored in the database and\n# override whatever is in this ConfigMap on every pod start. See details\n# in k3s-instructions.md §6.4.\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: aperio-config\n namespace: aperio\n labels:\n app.kubernetes.io/part-of: aperio\ndata:\n # ── AI Provider ─────────────────────────────────────────────\n # Uncomment and set before first deploy. If left commented, Aperio\n # defaults to llamacpp (can be changed later from Web UI → Settings).\n #\n # Options:\n # llamacpp → local, vendored llama-server (needs the model downloaded\n # into the pod's persistent volume, or point LLAMACPP_BASE_URL\n # at a llama-server running elsewhere on your network)\n # anthropic → cloud, needs ANTHROPIC_API_KEY (set in UI)\n # deepseek → cloud, needs DEEPSEEK_API_KEY (set in UI)\n # gemini → cloud, needs GEMINI_API_KEY (set in UI)\n # claude-code → uses Claude Pro/Max via Agent SDK\n # codex → uses Codex CLI\n #\n # AI_PROVIDER: \"llamacpp\"\n # LLAMACPP_BASE_URL: \"http://192.168.1.100:8080\"\n\n # ── Embeddings ──────────────────────────────────────────────\n # Options:\n # transformers → local (no API key, lightweight)\n # voyage → cloud (needs VOYAGE_API_KEY, set in UI)\n #\n # EMBEDDING_PROVIDER: \"transformers\"\n"}, "583aa69ec85927b0f3e0c4433b94db6564a1354c": {"t": 1785748586.1854687, "n": 1705, "h": "bae085e234f18568", "c": "---\n# ConfigMap — first-boot configuration for Aperio.\n#\n# EDIT THIS FILE BEFORE FIRST DEPLOY on your Pi. Uncomment the lines for\n# the provider you want and set the values. If everything stays commented,\n# Aperio uses its built-in defaults (llamacpp + transformers), then you can\n# change them via the Web UI → Settings afterward.\n#\n# Once you save settings in the Web UI, they're stored in the database and\n# override whatever is in this ConfigMap on every pod start. See details\n# in k3s-instructions.md §6.4.\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: aperio-config\n namespace: aperio\n labels:\n app.kubernetes.io/part-of: aperio\ndata:\n # ── AI Provider ─────────────────────────────────────────────\n # Uncomment and set before first deploy. If left commented, Aperio\n # defaults to llamacpp (can be changed later from Web UI → Settings).\n #\n # Options:\n # llamacpp → local, vendored llama-server (needs the model downloaded\n # into the pod's persistent volume, or point LLAMACPP_BASE_URL\n # at a llama-server running elsewhere on your network)\n # anthropic → cloud, needs ANTHROPIC_API_KEY (set in UI)\n # deepseek → cloud, needs DEEPSEEK_API_KEY (set in UI)\n # gemini → cloud, needs GEMINI_API_KEY (set in UI)\n # claude-code → uses Claude Pro/Max via Agent SDK\n # codex → uses Codex CLI\n #\n # AI_PROVIDER: \"llamacpp\"\n # LLAMACPP_BASE_URL: \"http://192.168.1.100:8080\"\n\n # ── Embeddings ──────────────────────────────────────────────\n # Options:\n # transformers → local (no API key, lightweight)\n # voyage → cloud (needs VOYAGE_API_KEY, set in UI)\n #\n # EMBEDDING_PROVIDER: \"transformers\"\n"}, "8262b9f3e3ba8a34da08ab59b484b07e901bf080": {"t": 1785748587.256178, "n": 1529, "h": "719c863c3cc541b6", "c": "---\n# ConfigMap — first-boot configuration for Aperio.\n#\n# EDIT THIS FILE BEFORE FIRST DEPLOY on your Pi. Uncomment the lines for\n# the provider you want and set the values. If everything stays commented,\n# Aperio uses its built-in defaults (ollama + transformers), then you can\n# change them via the Web UI → Settings afterward.\n#\n# Once you save settings in the Web UI, they're stored in the database and\n# override whatever is in this ConfigMap on every pod start. See details\n# in k3s-instructions.md §6.4.\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: aperio-config\n namespace: aperio\n labels:\n app.kubernetes.io/part-of: aperio\ndata:\n # ── AI Provider ─────────────────────────────────────────────\n # Uncomment and set before first deploy. If left commented, Aperio\n # defaults to ollama (can be changed later from Web UI → Settings).\n #\n # Options:\n # ollama → local, needs Ollama running on your network\n # anthropic → cloud, needs ANTHROPIC_API_KEY (set in UI)\n # deepseek → cloud, needs DEEPSEEK_API_KEY (set in UI)\n # gemini → cloud, needs GEMINI_API_KEY (set in UI)\n # claude-code → uses Claude Pro/Max via Agent SDK\n # codex → uses Codex CLI\n #\n # AI_PROVIDER: \"ollama\"\n # OLLAMA_BASE_URL: \"http://192.168.1.100:11434\"\n\n # ── Embeddings ──────────────────────────────────────────────\n # Options:\n # transformers → local (no API key, lightweight)\n # voyage → cloud (needs VOYAGE_API_KEY, set in UI)\n #\n # EMBEDDING_PROVIDER: \"transformers\"\n"}, "2b4d143ca069ef7b115bf38e8e1cdfba32acc8de": {"t": 1786067307.5567598, "n": 1705, "h": "bae085e234f18568", "c": "---\n# ConfigMap — first-boot configuration for Aperio.\n#\n# EDIT THIS FILE BEFORE FIRST DEPLOY on your Pi. Uncomment the lines for\n# the provider you want and set the values. If everything stays commented,\n# Aperio uses its built-in defaults (llamacpp + transformers), then you can\n# change them via the Web UI → Settings afterward.\n#\n# Once you save settings in the Web UI, they're stored in the database and\n# override whatever is in this ConfigMap on every pod start. See details\n# in k3s-instructions.md §6.4.\napiVersion: v1\nkind: ConfigMap\nmetadata:\n name: aperio-config\n namespace: aperio\n labels:\n app.kubernetes.io/part-of: aperio\ndata:\n # ── AI Provider ─────────────────────────────────────────────\n # Uncomment and set before first deploy. If left commented, Aperio\n # defaults to llamacpp (can be changed later from Web UI → Settings).\n #\n # Options:\n # llamacpp → local, vendored llama-server (needs the model downloaded\n # into the pod's persistent volume, or point LLAMACPP_BASE_URL\n # at a llama-server running elsewhere on your network)\n # anthropic → cloud, needs ANTHROPIC_API_KEY (set in UI)\n # deepseek → cloud, needs DEEPSEEK_API_KEY (set in UI)\n # gemini → cloud, needs GEMINI_API_KEY (set in UI)\n # claude-code → uses Claude Pro/Max via Agent SDK\n # codex → uses Codex CLI\n #\n # AI_PROVIDER: \"llamacpp\"\n # LLAMACPP_BASE_URL: \"http://192.168.1.100:8080\"\n\n # ── Embeddings ──────────────────────────────────────────────\n # Options:\n # transformers → local (no API key, lightweight)\n # voyage → cloud (needs VOYAGE_API_KEY, set in UI)\n #\n # EMBEDDING_PROVIDER: \"transformers\"\n"}}, "lib/handlers/wiki/regenerate.js": {"2b2a200280e92e939fc11bf60927fdf887f58cf7": {"t": 1785746698.3545232, "n": 12109, "h": "cc5d86b1e7521882", "c": "// lib/handlers/wiki/regenerate.js\n// Stale-article self-heal. Picks a dedicated, env-configured \"refresh provider\"\n// (see WIKI_REFRESH_PROVIDER in .env.example) and rewrites a stale article in one\n// non-tool-use LLM call. Falls back gracefully if not configured.\n//\n// Why not auto-pick the cheapest provider? Because if the user is on a cloud model\n// by choice, silently firing up the local engine is hostile. The refresh provider\n// must be opt-in via env, just like ROUNDTABLE_AGENTS.\n\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\nimport logger from \"../../helpers/logger.js\";\nimport { ensureLlamaCpp, getLoadedModels } from \"../../helpers/startLlamaCpp.js\";\nimport { LLAMACPP_MAIN_ALIAS } from \"../../helpers/llamacppAliases.js\";\nimport { resolveProvider } from \"../../providers/index.js\";\nimport { getArticle } from \"./wikiQueries.js\";\nimport { wikiWriteHandler } from \"./wikiHandlers.js\";\nimport { isVectorSearchable } from \"../../helpers/vecMeta.js\";\n\nconst SUPPORTED = new Set([\"llamacpp\", \"deepseek\", \"anthropic\", \"gemini\"]);\n\n// Parse \"WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\"\n// into { name, model } — or null if unset/malformed.\nexport function parseRefreshProvider(raw) {\n if (!raw || !raw.trim()) return null;\n const [name, ...rest] = raw.trim().split(\":\");\n const model = rest.join(\":\").trim();\n if (!name || !model) {\n logger.warn(`[wiki/refresh] WIKI_REFRESH_PROVIDER=\"${raw}\" — expected \"provider:model\"`);\n return null;\n }\n if (!SUPPORTED.has(name.toLowerCase())) {\n logger.warn(`[wiki/refresh] unsupported refresh provider \"${name}\" — must be one of ${[...SUPPORTED].join(\", \")}`);\n return null;\n }\n return { name: name.toLowerCase(), model };\n}\n\nexport function refreshRequestModel(model, mainProvider = resolveProvider({ name: \"llamacpp\" })) {\n return model === mainProvider?.model\n ? (mainProvider.requestModel || LLAMACPP_MAIN_ALIAS)\n : model;\n}\n\nexport async function checkLlamaCppModelServed(model, requestModel = model) {\n const payload = await getLoadedModels();\n if (!payload) return null;\n const served = new Set();\n for (const entry of payload.data ?? []) {\n if (entry?.id) served.add(entry.id);\n for (const alias of entry?.aliases ?? []) served.add(alias);\n }\n if (served.has(requestModel)) return null;\n const loaded = [...served].join(\", \") || \"none\";\n return `model not served: ${model}; loaded: ${loaded}. Restart Aperio to pick up the preset.`;\n}\n\n// One-shot text completion. Returns string. Throws on transport/API errors;\n// callers log and surface a structured failure.\nasync function complete({ name, model }, { system, user, maxTokens = 2048, requestModel }) {\n if (name === \"llamacpp\") {\n const base = process.env.LLAMACPP_BASE_URL || \"http://127.0.0.1:8080\";\n // Route through the resident preset alias when the configured refresh model\n // IS the main model: requesting the raw HF id makes llama.cpp's router load\n // a second, full-context copy (see lib/helpers/completion.js). A genuinely\n // different refresh model is honored as-is — the user opted into a 2nd load.\n requestModel ??= refreshRequestModel(model);\n const r = await fetch(`${base}/v1/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: requestModel,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`llama.cpp HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"deepseek\") {\n if (!process.env.DEEPSEEK_API_KEY) throw new Error(\"DEEPSEEK_API_KEY missing\");\n const r = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${process.env.DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`deepseek HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"anthropic\") {\n if (!process.env.ANTHROPIC_API_KEY) throw new Error(\"ANTHROPIC_API_KEY missing\");\n const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\n const msg = await client.messages.create({\n model,\n system,\n max_tokens: maxTokens,\n messages: [{ role: \"user\", content: user }],\n });\n return msg.content?.find(b => b.type === \"text\")?.text ?? \"\";\n }\n if (name === \"gemini\") {\n if (!process.env.GEMINI_API_KEY) throw new Error(\"GEMINI_API_KEY missing\");\n const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const m = genai.getGenerativeModel({ model, systemInstruction: system });\n const result = await m.generateContent(user);\n return result.response?.text?.() ?? \"\";\n }\n throw new Error(`unknown provider: ${name}`);\n}\n\nconst REWRITE_SYSTEM = `You are rewriting a stale wiki article using a fresh set of source memories.\n\nRULES (non-negotiable):\n- Output ONLY the markdown body. No preamble, no commentary, no code fences around the whole thing.\n- Cite every factual claim with an inline marker of the form [[mem:<uuid>]] using one of the provided memory ids.\n- Do NOT invent facts that aren't in the provided memories.\n- Keep the structure tight — short sections, no filler.\n- Preserve [[other-slug]] sibling links from the prior body where they still make sense.`;\n\nexport function buildUserPrompt(article, memories) {\n const memBlock = memories.length\n ? memories.map(m => `- id: ${m.id}\\n title: ${m.title}\\n content: ${m.content}`).join(\"\\n\\n\")\n : \"(no memories matched — keep the article minimal and flag the gap.)\";\n return [\n `# Article to refresh`,\n `Slug: ${article.slug}`,\n `Title: ${article.title}`,\n article.summary ? `Summary: ${article.summary}` : null,\n ``,\n `## Prior body (now stale)`,\n article.body_md,\n ``,\n `## Available source memories`,\n memBlock,\n ``,\n `Rewrite the body. Cite every claim as [[mem:<uuid>]] using the ids above.`,\n ].filter(Boolean).join(\"\\n\");\n}\n\n// Extract every [[mem:uuid]] marker, dedupe.\nexport function extractCitedMemoryIds(body) {\n const ids = new Set();\n const re = /\\[\\[mem:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\]\\]/gi;\n let m; while ((m = re.exec(body))) ids.add(m[1].toLowerCase());\n return [...ids];\n}\n\n/**\n * Regenerate a stale article in place.\n * Returns { ok: true, revision } on success or { ok: false, reason } on any failure.\n * Never throws — caller can use the return shape to keep serving the stale body.\n */\nexport async function regenerateArticle(ctx, slug) {\n const t0 = Date.now();\n const log = (level, msg, extra = {}) => logger[level](`[wiki/refresh] ${msg}`, { slug, ...extra });\n\n const cfg = parseRefreshProvider(process.env.WIKI_REFRESH_PROVIDER);\n if (!cfg) {\n log(\"warn\", \"WIKI_REFRESH_PROVIDER not set — refresh disabled\");\n return { ok: false, reason: \"refresh provider not configured (set WIKI_REFRESH_PROVIDER)\" };\n }\n\n log(\"info\", `starting regen via ${cfg.name}:${cfg.model}`);\n\n // Step 1: load the article.\n let article;\n try {\n article = await getArticle(ctx.store, slug);\n } catch (err) {\n log(\"error\", `load article failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `load failed: ${err.message}` };\n }\n if (!article) return { ok: false, reason: `article \"${slug}\" not found` };\n\n // Step 2: if the provider is the local engine and auto-start is on, ensure it's running.\n if (cfg.name === \"llamacpp\" && process.env.WIKI_REFRESH_AUTOSTART_LLAMACPP === \"true\") {\n try { await ensureLlamaCpp(); }\n catch (err) {\n log(\"error\", `ensureLlamaCpp failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `llama.cpp auto-start failed: ${err.message}` };\n }\n }\n\n if (cfg.name === \"llamacpp\") {\n const requestModel = refreshRequestModel(\n cfg.model,\n ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined,\n );\n const availabilityError = await checkLlamaCppModelServed(cfg.model, requestModel);\n if (availabilityError) {\n log(\"error\", availabilityError, { provider: cfg.name, model: cfg.model });\n return { ok: false, reason: availabilityError };\n }\n }\n\n // Step 3: gather fresh source memories.\n const recallQuery = [article.title, article.summary].filter(Boolean).join(\". \");\n let memories;\n try {\n // FTS-only while the memories store is stale/reindexing (issue #287).\n const queryEmbedding = await isVectorSearchable(ctx.store, \"memories\")\n ? await ctx.generateEmbedding(recallQuery)\n : null;\n memories = await ctx.store.recall({\n query: recallQuery,\n queryEmbedding,\n limit: 12,\n mode: queryEmbedding ? \"auto\" : \"fulltext\",\n });\n log(\"info\", `recall returned ${memories.length} memories`);\n } catch (err) {\n log(\"error\", `recall failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `recall failed: ${err.message}` };\n }\n\n if (!memories.length) {\n log(\"warn\", \"no memories matched — leaving article stale\");\n return { ok: false, reason: \"no source memories matched the article topic\" };\n }\n\n // Step 4: call the LLM.\n let body_md;\n try {\n body_md = await complete(cfg, {\n system: REWRITE_SYSTEM,\n user: buildUserPrompt(article, memories),\n maxTokens: 2048,\n requestModel: cfg.name === \"llamacpp\"\n ? refreshRequestModel(cfg.model, ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined)\n : undefined,\n });\n if (!body_md || !body_md.trim()) throw new Error(\"provider returned empty body\");\n log(\"info\", `completion ok (${body_md.length} chars)`);\n } catch (err) {\n log(\"error\", `LLM completion failed: ${err.message}`, { provider: cfg.name, model: cfg.model, stack: err.stack });\n return { ok: false, reason: `LLM completion failed: ${err.message}` };\n }\n\n // Step 5: extract citations. Refuse to write an article that cites nothing —\n // a zero-citation refresh almost certainly means the model ignored the rules.\n const citedIds = extractCitedMemoryIds(body_md);\n const validIds = new Set(memories.map(m => m.id));\n const grounded = citedIds.filter(id => validIds.has(id));\n if (!grounded.length) {\n log(\"warn\", `regenerated body cites no valid memories — discarding`, {\n citedCount: citedIds.length, validRecallCount: memories.length,\n });\n return { ok: false, reason: \"regenerated body had no valid [[mem:uuid]] citations\" };\n }\n if (grounded.length < citedIds.length) {\n log(\"warn\", `${citedIds.length - grounded.length} citation(s) referenced ids not in recall set — keeping the rest`);\n }\n\n // Step 6: write back via the existing handler (this triggers revision archival via the SQL trigger).\n try {\n const result = await wikiWriteHandler(ctx, {\n slug: article.slug,\n title: article.title,\n summary: article.summary,\n body_md,\n tags: article.tags,\n source_memory_ids: grounded,\n });\n // wikiWriteHandler returns { content: [{ text }] }; surface its text in logs.\n const text = result?.content?.[0]?.text ?? \"\";\n log(\"info\", `regen complete in ${Date.now() - t0}ms — ${text}`);\n return { ok: true, citations: grounded.length, ms: Date.now() - t0 };\n } catch (err) {\n log(\"error\", `wiki_write failed during refresh: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `write failed: ${err.message}` };\n }\n}\n"}, "8d14cddbbfdab28358b01daaa86e8ac2a556d660": {"t": 1785748588.1263337, "n": 12109, "h": "cc5d86b1e7521882", "c": "// lib/handlers/wiki/regenerate.js\n// Stale-article self-heal. Picks a dedicated, env-configured \"refresh provider\"\n// (see WIKI_REFRESH_PROVIDER in .env.example) and rewrites a stale article in one\n// non-tool-use LLM call. Falls back gracefully if not configured.\n//\n// Why not auto-pick the cheapest provider? Because if the user is on a cloud model\n// by choice, silently firing up the local engine is hostile. The refresh provider\n// must be opt-in via env, just like ROUNDTABLE_AGENTS.\n\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\nimport logger from \"../../helpers/logger.js\";\nimport { ensureLlamaCpp, getLoadedModels } from \"../../helpers/startLlamaCpp.js\";\nimport { LLAMACPP_MAIN_ALIAS } from \"../../helpers/llamacppAliases.js\";\nimport { resolveProvider } from \"../../providers/index.js\";\nimport { getArticle } from \"./wikiQueries.js\";\nimport { wikiWriteHandler } from \"./wikiHandlers.js\";\nimport { isVectorSearchable } from \"../../helpers/vecMeta.js\";\n\nconst SUPPORTED = new Set([\"llamacpp\", \"deepseek\", \"anthropic\", \"gemini\"]);\n\n// Parse \"WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\"\n// into { name, model } — or null if unset/malformed.\nexport function parseRefreshProvider(raw) {\n if (!raw || !raw.trim()) return null;\n const [name, ...rest] = raw.trim().split(\":\");\n const model = rest.join(\":\").trim();\n if (!name || !model) {\n logger.warn(`[wiki/refresh] WIKI_REFRESH_PROVIDER=\"${raw}\" — expected \"provider:model\"`);\n return null;\n }\n if (!SUPPORTED.has(name.toLowerCase())) {\n logger.warn(`[wiki/refresh] unsupported refresh provider \"${name}\" — must be one of ${[...SUPPORTED].join(\", \")}`);\n return null;\n }\n return { name: name.toLowerCase(), model };\n}\n\nexport function refreshRequestModel(model, mainProvider = resolveProvider({ name: \"llamacpp\" })) {\n return model === mainProvider?.model\n ? (mainProvider.requestModel || LLAMACPP_MAIN_ALIAS)\n : model;\n}\n\nexport async function checkLlamaCppModelServed(model, requestModel = model) {\n const payload = await getLoadedModels();\n if (!payload) return null;\n const served = new Set();\n for (const entry of payload.data ?? []) {\n if (entry?.id) served.add(entry.id);\n for (const alias of entry?.aliases ?? []) served.add(alias);\n }\n if (served.has(requestModel)) return null;\n const loaded = [...served].join(\", \") || \"none\";\n return `model not served: ${model}; loaded: ${loaded}. Restart Aperio to pick up the preset.`;\n}\n\n// One-shot text completion. Returns string. Throws on transport/API errors;\n// callers log and surface a structured failure.\nasync function complete({ name, model }, { system, user, maxTokens = 2048, requestModel }) {\n if (name === \"llamacpp\") {\n const base = process.env.LLAMACPP_BASE_URL || \"http://127.0.0.1:8080\";\n // Route through the resident preset alias when the configured refresh model\n // IS the main model: requesting the raw HF id makes llama.cpp's router load\n // a second, full-context copy (see lib/helpers/completion.js). A genuinely\n // different refresh model is honored as-is — the user opted into a 2nd load.\n requestModel ??= refreshRequestModel(model);\n const r = await fetch(`${base}/v1/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: requestModel,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`llama.cpp HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"deepseek\") {\n if (!process.env.DEEPSEEK_API_KEY) throw new Error(\"DEEPSEEK_API_KEY missing\");\n const r = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${process.env.DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`deepseek HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"anthropic\") {\n if (!process.env.ANTHROPIC_API_KEY) throw new Error(\"ANTHROPIC_API_KEY missing\");\n const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\n const msg = await client.messages.create({\n model,\n system,\n max_tokens: maxTokens,\n messages: [{ role: \"user\", content: user }],\n });\n return msg.content?.find(b => b.type === \"text\")?.text ?? \"\";\n }\n if (name === \"gemini\") {\n if (!process.env.GEMINI_API_KEY) throw new Error(\"GEMINI_API_KEY missing\");\n const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const m = genai.getGenerativeModel({ model, systemInstruction: system });\n const result = await m.generateContent(user);\n return result.response?.text?.() ?? \"\";\n }\n throw new Error(`unknown provider: ${name}`);\n}\n\nconst REWRITE_SYSTEM = `You are rewriting a stale wiki article using a fresh set of source memories.\n\nRULES (non-negotiable):\n- Output ONLY the markdown body. No preamble, no commentary, no code fences around the whole thing.\n- Cite every factual claim with an inline marker of the form [[mem:<uuid>]] using one of the provided memory ids.\n- Do NOT invent facts that aren't in the provided memories.\n- Keep the structure tight — short sections, no filler.\n- Preserve [[other-slug]] sibling links from the prior body where they still make sense.`;\n\nexport function buildUserPrompt(article, memories) {\n const memBlock = memories.length\n ? memories.map(m => `- id: ${m.id}\\n title: ${m.title}\\n content: ${m.content}`).join(\"\\n\\n\")\n : \"(no memories matched — keep the article minimal and flag the gap.)\";\n return [\n `# Article to refresh`,\n `Slug: ${article.slug}`,\n `Title: ${article.title}`,\n article.summary ? `Summary: ${article.summary}` : null,\n ``,\n `## Prior body (now stale)`,\n article.body_md,\n ``,\n `## Available source memories`,\n memBlock,\n ``,\n `Rewrite the body. Cite every claim as [[mem:<uuid>]] using the ids above.`,\n ].filter(Boolean).join(\"\\n\");\n}\n\n// Extract every [[mem:uuid]] marker, dedupe.\nexport function extractCitedMemoryIds(body) {\n const ids = new Set();\n const re = /\\[\\[mem:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\]\\]/gi;\n let m; while ((m = re.exec(body))) ids.add(m[1].toLowerCase());\n return [...ids];\n}\n\n/**\n * Regenerate a stale article in place.\n * Returns { ok: true, revision } on success or { ok: false, reason } on any failure.\n * Never throws — caller can use the return shape to keep serving the stale body.\n */\nexport async function regenerateArticle(ctx, slug) {\n const t0 = Date.now();\n const log = (level, msg, extra = {}) => logger[level](`[wiki/refresh] ${msg}`, { slug, ...extra });\n\n const cfg = parseRefreshProvider(process.env.WIKI_REFRESH_PROVIDER);\n if (!cfg) {\n log(\"warn\", \"WIKI_REFRESH_PROVIDER not set — refresh disabled\");\n return { ok: false, reason: \"refresh provider not configured (set WIKI_REFRESH_PROVIDER)\" };\n }\n\n log(\"info\", `starting regen via ${cfg.name}:${cfg.model}`);\n\n // Step 1: load the article.\n let article;\n try {\n article = await getArticle(ctx.store, slug);\n } catch (err) {\n log(\"error\", `load article failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `load failed: ${err.message}` };\n }\n if (!article) return { ok: false, reason: `article \"${slug}\" not found` };\n\n // Step 2: if the provider is the local engine and auto-start is on, ensure it's running.\n if (cfg.name === \"llamacpp\" && process.env.WIKI_REFRESH_AUTOSTART_LLAMACPP === \"true\") {\n try { await ensureLlamaCpp(); }\n catch (err) {\n log(\"error\", `ensureLlamaCpp failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `llama.cpp auto-start failed: ${err.message}` };\n }\n }\n\n if (cfg.name === \"llamacpp\") {\n const requestModel = refreshRequestModel(\n cfg.model,\n ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined,\n );\n const availabilityError = await checkLlamaCppModelServed(cfg.model, requestModel);\n if (availabilityError) {\n log(\"error\", availabilityError, { provider: cfg.name, model: cfg.model });\n return { ok: false, reason: availabilityError };\n }\n }\n\n // Step 3: gather fresh source memories.\n const recallQuery = [article.title, article.summary].filter(Boolean).join(\". \");\n let memories;\n try {\n // FTS-only while the memories store is stale/reindexing (issue #287).\n const queryEmbedding = await isVectorSearchable(ctx.store, \"memories\")\n ? await ctx.generateEmbedding(recallQuery)\n : null;\n memories = await ctx.store.recall({\n query: recallQuery,\n queryEmbedding,\n limit: 12,\n mode: queryEmbedding ? \"auto\" : \"fulltext\",\n });\n log(\"info\", `recall returned ${memories.length} memories`);\n } catch (err) {\n log(\"error\", `recall failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `recall failed: ${err.message}` };\n }\n\n if (!memories.length) {\n log(\"warn\", \"no memories matched — leaving article stale\");\n return { ok: false, reason: \"no source memories matched the article topic\" };\n }\n\n // Step 4: call the LLM.\n let body_md;\n try {\n body_md = await complete(cfg, {\n system: REWRITE_SYSTEM,\n user: buildUserPrompt(article, memories),\n maxTokens: 2048,\n requestModel: cfg.name === \"llamacpp\"\n ? refreshRequestModel(cfg.model, ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined)\n : undefined,\n });\n if (!body_md || !body_md.trim()) throw new Error(\"provider returned empty body\");\n log(\"info\", `completion ok (${body_md.length} chars)`);\n } catch (err) {\n log(\"error\", `LLM completion failed: ${err.message}`, { provider: cfg.name, model: cfg.model, stack: err.stack });\n return { ok: false, reason: `LLM completion failed: ${err.message}` };\n }\n\n // Step 5: extract citations. Refuse to write an article that cites nothing —\n // a zero-citation refresh almost certainly means the model ignored the rules.\n const citedIds = extractCitedMemoryIds(body_md);\n const validIds = new Set(memories.map(m => m.id));\n const grounded = citedIds.filter(id => validIds.has(id));\n if (!grounded.length) {\n log(\"warn\", `regenerated body cites no valid memories — discarding`, {\n citedCount: citedIds.length, validRecallCount: memories.length,\n });\n return { ok: false, reason: \"regenerated body had no valid [[mem:uuid]] citations\" };\n }\n if (grounded.length < citedIds.length) {\n log(\"warn\", `${citedIds.length - grounded.length} citation(s) referenced ids not in recall set — keeping the rest`);\n }\n\n // Step 6: write back via the existing handler (this triggers revision archival via the SQL trigger).\n try {\n const result = await wikiWriteHandler(ctx, {\n slug: article.slug,\n title: article.title,\n summary: article.summary,\n body_md,\n tags: article.tags,\n source_memory_ids: grounded,\n });\n // wikiWriteHandler returns { content: [{ text }] }; surface its text in logs.\n const text = result?.content?.[0]?.text ?? \"\";\n log(\"info\", `regen complete in ${Date.now() - t0}ms — ${text}`);\n return { ok: true, citations: grounded.length, ms: Date.now() - t0 };\n } catch (err) {\n log(\"error\", `wiki_write failed during refresh: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `write failed: ${err.message}` };\n }\n}\n"}, "6403f4ef57e1822d9e2fd452366d4547362ec280": {"t": 1785748589.2304347, "n": 11902, "h": "9ac2ec1358470272", "c": "// lib/handlers/wiki/regenerate.js\n// Stale-article self-heal. Picks a dedicated, env-configured \"refresh provider\"\n// (see WIKI_REFRESH_PROVIDER in .env.example) and rewrites a stale article in one\n// non-tool-use LLM call. Falls back gracefully if not configured.\n//\n// Why not auto-pick the cheapest provider? Because if the user is on a cloud model\n// by choice, silently firing up the local engine is hostile. The refresh provider\n// must be opt-in via env, just like ROUNDTABLE_AGENTS.\n\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\nimport logger from \"../../helpers/logger.js\";\nimport { ensureLlamaCpp, getLoadedModels } from \"../../helpers/startLlamaCpp.js\";\nimport { LLAMACPP_MAIN_ALIAS } from \"../../helpers/llamacppAliases.js\";\nimport { resolveProvider } from \"../../providers/index.js\";\nimport { getArticle } from \"./wikiQueries.js\";\nimport { wikiWriteHandler } from \"./wikiHandlers.js\";\n\nconst SUPPORTED = new Set([\"llamacpp\", \"deepseek\", \"anthropic\", \"gemini\"]);\n\n// Parse \"WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\"\n// into { name, model } — or null if unset/malformed.\nexport function parseRefreshProvider(raw) {\n if (!raw || !raw.trim()) return null;\n const [name, ...rest] = raw.trim().split(\":\");\n const model = rest.join(\":\").trim();\n if (!name || !model) {\n logger.warn(`[wiki/refresh] WIKI_REFRESH_PROVIDER=\"${raw}\" — expected \"provider:model\"`);\n return null;\n }\n if (!SUPPORTED.has(name.toLowerCase())) {\n logger.warn(`[wiki/refresh] unsupported refresh provider \"${name}\" — must be one of ${[...SUPPORTED].join(\", \")}`);\n return null;\n }\n return { name: name.toLowerCase(), model };\n}\n\nexport function refreshRequestModel(model, mainProvider = resolveProvider({ name: \"llamacpp\" })) {\n return model === mainProvider?.model\n ? (mainProvider.requestModel || LLAMACPP_MAIN_ALIAS)\n : model;\n}\n\nexport async function checkLlamaCppModelServed(model, requestModel = model) {\n const payload = await getLoadedModels();\n if (!payload) return null;\n const served = new Set();\n for (const entry of payload.data ?? []) {\n if (entry?.id) served.add(entry.id);\n for (const alias of entry?.aliases ?? []) served.add(alias);\n }\n if (served.has(requestModel)) return null;\n const loaded = [...served].join(\", \") || \"none\";\n return `model not served: ${model}; loaded: ${loaded}. Restart Aperio to pick up the preset.`;\n}\n\n// One-shot text completion. Returns string. Throws on transport/API errors;\n// callers log and surface a structured failure.\nasync function complete({ name, model }, { system, user, maxTokens = 2048, requestModel }) {\n if (name === \"llamacpp\") {\n const base = process.env.LLAMACPP_BASE_URL || \"http://127.0.0.1:8080\";\n // Route through the resident preset alias when the configured refresh model\n // IS the main model: requesting the raw HF id makes llama.cpp's router load\n // a second, full-context copy (see lib/helpers/completion.js). A genuinely\n // different refresh model is honored as-is — the user opted into a 2nd load.\n requestModel ??= refreshRequestModel(model);\n const r = await fetch(`${base}/v1/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: requestModel,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`llama.cpp HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"deepseek\") {\n if (!process.env.DEEPSEEK_API_KEY) throw new Error(\"DEEPSEEK_API_KEY missing\");\n const r = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${process.env.DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`deepseek HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"anthropic\") {\n if (!process.env.ANTHROPIC_API_KEY) throw new Error(\"ANTHROPIC_API_KEY missing\");\n const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\n const msg = await client.messages.create({\n model,\n system,\n max_tokens: maxTokens,\n messages: [{ role: \"user\", content: user }],\n });\n return msg.content?.find(b => b.type === \"text\")?.text ?? \"\";\n }\n if (name === \"gemini\") {\n if (!process.env.GEMINI_API_KEY) throw new Error(\"GEMINI_API_KEY missing\");\n const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const m = genai.getGenerativeModel({ model, systemInstruction: system });\n const result = await m.generateContent(user);\n return result.response?.text?.() ?? \"\";\n }\n throw new Error(`unknown provider: ${name}`);\n}\n\nconst REWRITE_SYSTEM = `You are rewriting a stale wiki article using a fresh set of source memories.\n\nRULES (non-negotiable):\n- Output ONLY the markdown body. No preamble, no commentary, no code fences around the whole thing.\n- Cite every factual claim with an inline marker of the form [[mem:<uuid>]] using one of the provided memory ids.\n- Do NOT invent facts that aren't in the provided memories.\n- Keep the structure tight — short sections, no filler.\n- Preserve [[other-slug]] sibling links from the prior body where they still make sense.`;\n\nexport function buildUserPrompt(article, memories) {\n const memBlock = memories.length\n ? memories.map(m => `- id: ${m.id}\\n title: ${m.title}\\n content: ${m.content}`).join(\"\\n\\n\")\n : \"(no memories matched — keep the article minimal and flag the gap.)\";\n return [\n `# Article to refresh`,\n `Slug: ${article.slug}`,\n `Title: ${article.title}`,\n article.summary ? `Summary: ${article.summary}` : null,\n ``,\n `## Prior body (now stale)`,\n article.body_md,\n ``,\n `## Available source memories`,\n memBlock,\n ``,\n `Rewrite the body. Cite every claim as [[mem:<uuid>]] using the ids above.`,\n ].filter(Boolean).join(\"\\n\");\n}\n\n// Extract every [[mem:uuid]] marker, dedupe.\nexport function extractCitedMemoryIds(body) {\n const ids = new Set();\n const re = /\\[\\[mem:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\]\\]/gi;\n let m; while ((m = re.exec(body))) ids.add(m[1].toLowerCase());\n return [...ids];\n}\n\n/**\n * Regenerate a stale article in place.\n * Returns { ok: true, revision } on success or { ok: false, reason } on any failure.\n * Never throws — caller can use the return shape to keep serving the stale body.\n */\nexport async function regenerateArticle(ctx, slug) {\n const t0 = Date.now();\n const log = (level, msg, extra = {}) => logger[level](`[wiki/refresh] ${msg}`, { slug, ...extra });\n\n const cfg = parseRefreshProvider(process.env.WIKI_REFRESH_PROVIDER);\n if (!cfg) {\n log(\"warn\", \"WIKI_REFRESH_PROVIDER not set — refresh disabled\");\n return { ok: false, reason: \"refresh provider not configured (set WIKI_REFRESH_PROVIDER)\" };\n }\n\n log(\"info\", `starting regen via ${cfg.name}:${cfg.model}`);\n\n // Step 1: load the article.\n let article;\n try {\n article = await getArticle(ctx.store, slug);\n } catch (err) {\n log(\"error\", `load article failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `load failed: ${err.message}` };\n }\n if (!article) return { ok: false, reason: `article \"${slug}\" not found` };\n\n // Step 2: if the provider is the local engine and auto-start is on, ensure it's running.\n if (cfg.name === \"llamacpp\" && process.env.WIKI_REFRESH_AUTOSTART_LLAMACPP === \"true\") {\n try { await ensureLlamaCpp(); }\n catch (err) {\n log(\"error\", `ensureLlamaCpp failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `llama.cpp auto-start failed: ${err.message}` };\n }\n }\n\n if (cfg.name === \"llamacpp\") {\n const requestModel = refreshRequestModel(\n cfg.model,\n ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined,\n );\n const availabilityError = await checkLlamaCppModelServed(cfg.model, requestModel);\n if (availabilityError) {\n log(\"error\", availabilityError, { provider: cfg.name, model: cfg.model });\n return { ok: false, reason: availabilityError };\n }\n }\n\n // Step 3: gather fresh source memories.\n const recallQuery = [article.title, article.summary].filter(Boolean).join(\". \");\n let memories;\n try {\n const queryEmbedding = await ctx.generateEmbedding(recallQuery);\n memories = await ctx.store.recall({\n query: recallQuery,\n queryEmbedding,\n limit: 12,\n mode: queryEmbedding ? \"auto\" : \"fulltext\",\n });\n log(\"info\", `recall returned ${memories.length} memories`);\n } catch (err) {\n log(\"error\", `recall failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `recall failed: ${err.message}` };\n }\n\n if (!memories.length) {\n log(\"warn\", \"no memories matched — leaving article stale\");\n return { ok: false, reason: \"no source memories matched the article topic\" };\n }\n\n // Step 4: call the LLM.\n let body_md;\n try {\n body_md = await complete(cfg, {\n system: REWRITE_SYSTEM,\n user: buildUserPrompt(article, memories),\n maxTokens: 2048,\n requestModel: cfg.name === \"llamacpp\"\n ? refreshRequestModel(cfg.model, ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined)\n : undefined,\n });\n if (!body_md || !body_md.trim()) throw new Error(\"provider returned empty body\");\n log(\"info\", `completion ok (${body_md.length} chars)`);\n } catch (err) {\n log(\"error\", `LLM completion failed: ${err.message}`, { provider: cfg.name, model: cfg.model, stack: err.stack });\n return { ok: false, reason: `LLM completion failed: ${err.message}` };\n }\n\n // Step 5: extract citations. Refuse to write an article that cites nothing —\n // a zero-citation refresh almost certainly means the model ignored the rules.\n const citedIds = extractCitedMemoryIds(body_md);\n const validIds = new Set(memories.map(m => m.id));\n const grounded = citedIds.filter(id => validIds.has(id));\n if (!grounded.length) {\n log(\"warn\", `regenerated body cites no valid memories — discarding`, {\n citedCount: citedIds.length, validRecallCount: memories.length,\n });\n return { ok: false, reason: \"regenerated body had no valid [[mem:uuid]] citations\" };\n }\n if (grounded.length < citedIds.length) {\n log(\"warn\", `${citedIds.length - grounded.length} citation(s) referenced ids not in recall set — keeping the rest`);\n }\n\n // Step 6: write back via the existing handler (this triggers revision archival via the SQL trigger).\n try {\n const result = await wikiWriteHandler(ctx, {\n slug: article.slug,\n title: article.title,\n summary: article.summary,\n body_md,\n tags: article.tags,\n source_memory_ids: grounded,\n });\n // wikiWriteHandler returns { content: [{ text }] }; surface its text in logs.\n const text = result?.content?.[0]?.text ?? \"\";\n log(\"info\", `regen complete in ${Date.now() - t0}ms — ${text}`);\n return { ok: true, citations: grounded.length, ms: Date.now() - t0 };\n } catch (err) {\n log(\"error\", `wiki_write failed during refresh: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `write failed: ${err.message}` };\n }\n}\n"}, "49ff1ba5e5c1a7b8dedc03129cadec38c9fd02be": {"t": 1785748590.3392408, "n": 11888, "h": "500c0a52c75adf0f", "c": "// lib/handlers/wiki/regenerate.js\n// Stale-article self-heal. Picks a dedicated, env-configured \"refresh provider\"\n// (see WIKI_REFRESH_PROVIDER in .env.example) and rewrites a stale article in one\n// non-tool-use LLM call. Falls back gracefully if not configured.\n//\n// Why not auto-pick the cheapest provider? Because if the user is on a cloud model\n// by choice, silently firing up the local engine is hostile. The refresh provider\n// must be opt-in via env, just like ROUNDTABLE_AGENTS.\n\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\nimport logger from \"../../helpers/logger.js\";\nimport { ensureLlamaCpp, getLoadedModels } from \"../../helpers/startLlamaCpp.js\";\nimport { LLAMACPP_MAIN_ALIAS } from \"../../helpers/llamacppAliases.js\";\nimport { resolveProvider } from \"../../providers/index.js\";\nimport { getArticle } from \"./wikiQueries.js\";\nimport { wikiWriteHandler } from \"./wikiHandlers.js\";\n\nconst SUPPORTED = new Set([\"llamacpp\", \"deepseek\", \"anthropic\", \"gemini\"]);\n\n// Parse \"WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\"\n// into { name, model } — or null if unset/malformed.\nexport function parseRefreshProvider(raw) {\n if (!raw || !raw.trim()) return null;\n const [name, ...rest] = raw.trim().split(\":\");\n const model = rest.join(\":\").trim();\n if (!name || !model) {\n logger.warn(`[wiki/refresh] WIKI_REFRESH_PROVIDER=\"${raw}\" — expected \"provider:model\"`);\n return null;\n }\n if (!SUPPORTED.has(name.toLowerCase())) {\n logger.warn(`[wiki/refresh] unsupported refresh provider \"${name}\" — must be one of ${[...SUPPORTED].join(\", \")}`);\n return null;\n }\n return { name: name.toLowerCase(), model };\n}\n\nexport function refreshRequestModel(model, mainProvider = resolveProvider({ name: \"llamacpp\" })) {\n return model === mainProvider?.model\n ? (mainProvider.requestModel || LLAMACPP_MAIN_ALIAS)\n : model;\n}\n\nexport async function checkLlamaCppModelServed(model, requestModel = model) {\n const payload = await getLoadedModels();\n if (!payload) return null;\n const served = new Set();\n for (const entry of payload.data ?? []) {\n if (entry?.id) served.add(entry.id);\n for (const alias of entry?.aliases ?? []) served.add(alias);\n }\n if (served.has(requestModel)) return null;\n const loaded = [...served].join(\", \") || \"none\";\n return `model not served: ${model}; loaded: ${loaded}. Restart Aperio to pick up the preset.`;\n}\n\n// One-shot text completion. Returns string. Throws on transport/API errors;\n// callers log and surface a structured failure.\nasync function complete({ name, model }, { system, user, maxTokens = 2048, requestModel }) {\n if (name === \"llamacpp\") {\n const base = process.env.LLAMACPP_BASE_URL || \"http://127.0.0.1:8080\";\n // Route through the resident preset alias when the configured refresh model\n // IS the main model: requesting the raw HF id makes llama.cpp's router load\n // a second, full-context copy (see lib/helpers/completion.js). A genuinely\n // different refresh model is honored as-is — the user opted into a 2nd load.\n requestModel ??= refreshRequestModel(model);\n const r = await fetch(`${base}/v1/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: requestModel,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`llama.cpp HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"deepseek\") {\n if (!process.env.DEEPSEEK_API_KEY) throw new Error(\"DEEPSEEK_API_KEY missing\");\n const r = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${process.env.DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`deepseek HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"anthropic\") {\n if (!process.env.ANTHROPIC_API_KEY) throw new Error(\"ANTHROPIC_API_KEY missing\");\n const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\n const msg = await client.messages.create({\n model,\n system,\n max_tokens: maxTokens,\n messages: [{ role: \"user\", content: user }],\n });\n return msg.content?.find(b => b.type === \"text\")?.text ?? \"\";\n }\n if (name === \"gemini\") {\n if (!process.env.GEMINI_API_KEY) throw new Error(\"GEMINI_API_KEY missing\");\n const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const m = genai.getGenerativeModel({ model, systemInstruction: system });\n const result = await m.generateContent(user);\n return result.response?.text?.() ?? \"\";\n }\n throw new Error(`unknown provider: ${name}`);\n}\n\nconst REWRITE_SYSTEM = `You are rewriting a stale wiki article using a fresh set of source memories.\n\nRULES (non-negotiable):\n- Output ONLY the markdown body. No preamble, no commentary, no code fences around the whole thing.\n- Cite every factual claim with an inline marker of the form [[mem:<uuid>]] using one of the provided memory ids.\n- Do NOT invent facts that aren't in the provided memories.\n- Keep the structure tight — short sections, no filler.\n- Preserve [[other-slug]] sibling links from the prior body where they still make sense.`;\n\nfunction buildUserPrompt(article, memories) {\n const memBlock = memories.length\n ? memories.map(m => `- id: ${m.id}\\n title: ${m.title}\\n content: ${m.content}`).join(\"\\n\\n\")\n : \"(no memories matched — keep the article minimal and flag the gap.)\";\n return [\n `# Article to refresh`,\n `Slug: ${article.slug}`,\n `Title: ${article.title}`,\n article.summary ? `Summary: ${article.summary}` : null,\n ``,\n `## Prior body (now stale)`,\n article.body_md,\n ``,\n `## Available source memories`,\n memBlock,\n ``,\n `Rewrite the body. Cite every claim as [[mem:<uuid>]] using the ids above.`,\n ].filter(Boolean).join(\"\\n\");\n}\n\n// Extract every [[mem:uuid]] marker, dedupe.\nfunction extractCitedMemoryIds(body) {\n const ids = new Set();\n const re = /\\[\\[mem:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\]\\]/gi;\n let m; while ((m = re.exec(body))) ids.add(m[1].toLowerCase());\n return [...ids];\n}\n\n/**\n * Regenerate a stale article in place.\n * Returns { ok: true, revision } on success or { ok: false, reason } on any failure.\n * Never throws — caller can use the return shape to keep serving the stale body.\n */\nexport async function regenerateArticle(ctx, slug) {\n const t0 = Date.now();\n const log = (level, msg, extra = {}) => logger[level](`[wiki/refresh] ${msg}`, { slug, ...extra });\n\n const cfg = parseRefreshProvider(process.env.WIKI_REFRESH_PROVIDER);\n if (!cfg) {\n log(\"warn\", \"WIKI_REFRESH_PROVIDER not set — refresh disabled\");\n return { ok: false, reason: \"refresh provider not configured (set WIKI_REFRESH_PROVIDER)\" };\n }\n\n log(\"info\", `starting regen via ${cfg.name}:${cfg.model}`);\n\n // Step 1: load the article.\n let article;\n try {\n article = await getArticle(ctx.store, slug);\n } catch (err) {\n log(\"error\", `load article failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `load failed: ${err.message}` };\n }\n if (!article) return { ok: false, reason: `article \"${slug}\" not found` };\n\n // Step 2: if the provider is the local engine and auto-start is on, ensure it's running.\n if (cfg.name === \"llamacpp\" && process.env.WIKI_REFRESH_AUTOSTART_LLAMACPP === \"true\") {\n try { await ensureLlamaCpp(); }\n catch (err) {\n log(\"error\", `ensureLlamaCpp failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `llama.cpp auto-start failed: ${err.message}` };\n }\n }\n\n if (cfg.name === \"llamacpp\") {\n const requestModel = refreshRequestModel(\n cfg.model,\n ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined,\n );\n const availabilityError = await checkLlamaCppModelServed(cfg.model, requestModel);\n if (availabilityError) {\n log(\"error\", availabilityError, { provider: cfg.name, model: cfg.model });\n return { ok: false, reason: availabilityError };\n }\n }\n\n // Step 3: gather fresh source memories.\n const recallQuery = [article.title, article.summary].filter(Boolean).join(\". \");\n let memories;\n try {\n const queryEmbedding = await ctx.generateEmbedding(recallQuery);\n memories = await ctx.store.recall({\n query: recallQuery,\n queryEmbedding,\n limit: 12,\n mode: queryEmbedding ? \"auto\" : \"fulltext\",\n });\n log(\"info\", `recall returned ${memories.length} memories`);\n } catch (err) {\n log(\"error\", `recall failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `recall failed: ${err.message}` };\n }\n\n if (!memories.length) {\n log(\"warn\", \"no memories matched — leaving article stale\");\n return { ok: false, reason: \"no source memories matched the article topic\" };\n }\n\n // Step 4: call the LLM.\n let body_md;\n try {\n body_md = await complete(cfg, {\n system: REWRITE_SYSTEM,\n user: buildUserPrompt(article, memories),\n maxTokens: 2048,\n requestModel: cfg.name === \"llamacpp\"\n ? refreshRequestModel(cfg.model, ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined)\n : undefined,\n });\n if (!body_md || !body_md.trim()) throw new Error(\"provider returned empty body\");\n log(\"info\", `completion ok (${body_md.length} chars)`);\n } catch (err) {\n log(\"error\", `LLM completion failed: ${err.message}`, { provider: cfg.name, model: cfg.model, stack: err.stack });\n return { ok: false, reason: `LLM completion failed: ${err.message}` };\n }\n\n // Step 5: extract citations. Refuse to write an article that cites nothing —\n // a zero-citation refresh almost certainly means the model ignored the rules.\n const citedIds = extractCitedMemoryIds(body_md);\n const validIds = new Set(memories.map(m => m.id));\n const grounded = citedIds.filter(id => validIds.has(id));\n if (!grounded.length) {\n log(\"warn\", `regenerated body cites no valid memories — discarding`, {\n citedCount: citedIds.length, validRecallCount: memories.length,\n });\n return { ok: false, reason: \"regenerated body had no valid [[mem:uuid]] citations\" };\n }\n if (grounded.length < citedIds.length) {\n log(\"warn\", `${citedIds.length - grounded.length} citation(s) referenced ids not in recall set — keeping the rest`);\n }\n\n // Step 6: write back via the existing handler (this triggers revision archival via the SQL trigger).\n try {\n const result = await wikiWriteHandler(ctx, {\n slug: article.slug,\n title: article.title,\n summary: article.summary,\n body_md,\n tags: article.tags,\n source_memory_ids: grounded,\n });\n // wikiWriteHandler returns { content: [{ text }] }; surface its text in logs.\n const text = result?.content?.[0]?.text ?? \"\";\n log(\"info\", `regen complete in ${Date.now() - t0}ms — ${text}`);\n return { ok: true, citations: grounded.length, ms: Date.now() - t0 };\n } catch (err) {\n log(\"error\", `wiki_write failed during refresh: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `write failed: ${err.message}` };\n }\n}\n"}, "12b1a564d9a459728c18717228f88a89a75f4efd": {"t": 1785748591.657694, "n": 10908, "h": "b7c0c5322941e7c6", "c": "// lib/handlers/wiki/regenerate.js\n// Stale-article self-heal. Picks a dedicated, env-configured \"refresh provider\"\n// (see WIKI_REFRESH_PROVIDER in .env.example) and rewrites a stale article in one\n// non-tool-use LLM call. Falls back gracefully if not configured.\n//\n// Why not auto-pick the cheapest provider? Because if the user is on a cloud model\n// by choice, silently firing up the local engine is hostile. The refresh provider\n// must be opt-in via env, just like ROUNDTABLE_AGENTS.\n\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\nimport logger from \"../../helpers/logger.js\";\nimport { ensureLlamaCpp } from \"../../helpers/startLlamaCpp.js\";\nimport { LLAMACPP_MAIN_ALIAS } from \"../../helpers/llamacppAliases.js\";\nimport { resolveProvider } from \"../../providers/index.js\";\nimport { getArticle } from \"./wikiQueries.js\";\nimport { wikiWriteHandler } from \"./wikiHandlers.js\";\n\nconst SUPPORTED = new Set([\"llamacpp\", \"deepseek\", \"anthropic\", \"gemini\"]);\n\n// Parse \"WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\"\n// into { name, model } — or null if unset/malformed.\nexport function parseRefreshProvider(raw) {\n if (!raw || !raw.trim()) return null;\n const [name, ...rest] = raw.trim().split(\":\");\n const model = rest.join(\":\").trim();\n if (!name || !model) {\n logger.warn(`[wiki/refresh] WIKI_REFRESH_PROVIDER=\"${raw}\" — expected \"provider:model\"`);\n return null;\n }\n if (!SUPPORTED.has(name.toLowerCase())) {\n logger.warn(`[wiki/refresh] unsupported refresh provider \"${name}\" — must be one of ${[...SUPPORTED].join(\", \")}`);\n return null;\n }\n return { name: name.toLowerCase(), model };\n}\n\nexport function refreshRequestModel(model, mainProvider = resolveProvider({ name: \"llamacpp\" })) {\n return model === mainProvider?.model\n ? (mainProvider.requestModel || LLAMACPP_MAIN_ALIAS)\n : model;\n}\n\n// One-shot text completion. Returns string. Throws on transport/API errors;\n// callers log and surface a structured failure.\nasync function complete({ name, model }, { system, user, maxTokens = 2048, requestModel }) {\n if (name === \"llamacpp\") {\n const base = process.env.LLAMACPP_BASE_URL || \"http://127.0.0.1:8080\";\n // Route through the resident preset alias when the configured refresh model\n // IS the main model: requesting the raw HF id makes llama.cpp's router load\n // a second, full-context copy (see lib/helpers/completion.js). A genuinely\n // different refresh model is honored as-is — the user opted into a 2nd load.\n requestModel ??= refreshRequestModel(model);\n const r = await fetch(`${base}/v1/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: requestModel,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`llama.cpp HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"deepseek\") {\n if (!process.env.DEEPSEEK_API_KEY) throw new Error(\"DEEPSEEK_API_KEY missing\");\n const r = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${process.env.DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`deepseek HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"anthropic\") {\n if (!process.env.ANTHROPIC_API_KEY) throw new Error(\"ANTHROPIC_API_KEY missing\");\n const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\n const msg = await client.messages.create({\n model,\n system,\n max_tokens: maxTokens,\n messages: [{ role: \"user\", content: user }],\n });\n return msg.content?.find(b => b.type === \"text\")?.text ?? \"\";\n }\n if (name === \"gemini\") {\n if (!process.env.GEMINI_API_KEY) throw new Error(\"GEMINI_API_KEY missing\");\n const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const m = genai.getGenerativeModel({ model, systemInstruction: system });\n const result = await m.generateContent(user);\n return result.response?.text?.() ?? \"\";\n }\n throw new Error(`unknown provider: ${name}`);\n}\n\nconst REWRITE_SYSTEM = `You are rewriting a stale wiki article using a fresh set of source memories.\n\nRULES (non-negotiable):\n- Output ONLY the markdown body. No preamble, no commentary, no code fences around the whole thing.\n- Cite every factual claim with an inline marker of the form [[mem:<uuid>]] using one of the provided memory ids.\n- Do NOT invent facts that aren't in the provided memories.\n- Keep the structure tight — short sections, no filler.\n- Preserve [[other-slug]] sibling links from the prior body where they still make sense.`;\n\nfunction buildUserPrompt(article, memories) {\n const memBlock = memories.length\n ? memories.map(m => `- id: ${m.id}\\n title: ${m.title}\\n content: ${m.content}`).join(\"\\n\\n\")\n : \"(no memories matched — keep the article minimal and flag the gap.)\";\n return [\n `# Article to refresh`,\n `Slug: ${article.slug}`,\n `Title: ${article.title}`,\n article.summary ? `Summary: ${article.summary}` : null,\n ``,\n `## Prior body (now stale)`,\n article.body_md,\n ``,\n `## Available source memories`,\n memBlock,\n ``,\n `Rewrite the body. Cite every claim as [[mem:<uuid>]] using the ids above.`,\n ].filter(Boolean).join(\"\\n\");\n}\n\n// Extract every [[mem:uuid]] marker, dedupe.\nfunction extractCitedMemoryIds(body) {\n const ids = new Set();\n const re = /\\[\\[mem:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\]\\]/gi;\n let m; while ((m = re.exec(body))) ids.add(m[1].toLowerCase());\n return [...ids];\n}\n\n/**\n * Regenerate a stale article in place.\n * Returns { ok: true, revision } on success or { ok: false, reason } on any failure.\n * Never throws — caller can use the return shape to keep serving the stale body.\n */\nexport async function regenerateArticle(ctx, slug) {\n const t0 = Date.now();\n const log = (level, msg, extra = {}) => logger[level](`[wiki/refresh] ${msg}`, { slug, ...extra });\n\n const cfg = parseRefreshProvider(process.env.WIKI_REFRESH_PROVIDER);\n if (!cfg) {\n log(\"warn\", \"WIKI_REFRESH_PROVIDER not set — refresh disabled\");\n return { ok: false, reason: \"refresh provider not configured (set WIKI_REFRESH_PROVIDER)\" };\n }\n\n log(\"info\", `starting regen via ${cfg.name}:${cfg.model}`);\n\n // Step 1: load the article.\n let article;\n try {\n article = await getArticle(ctx.store, slug);\n } catch (err) {\n log(\"error\", `load article failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `load failed: ${err.message}` };\n }\n if (!article) return { ok: false, reason: `article \"${slug}\" not found` };\n\n // Step 2: if the provider is the local engine and auto-start is on, ensure it's running.\n if (cfg.name === \"llamacpp\" && process.env.WIKI_REFRESH_AUTOSTART_LLAMACPP === \"true\") {\n try { await ensureLlamaCpp(); }\n catch (err) {\n log(\"error\", `ensureLlamaCpp failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `llama.cpp auto-start failed: ${err.message}` };\n }\n }\n\n // Step 3: gather fresh source memories.\n const recallQuery = [article.title, article.summary].filter(Boolean).join(\". \");\n let memories;\n try {\n const queryEmbedding = await ctx.generateEmbedding(recallQuery);\n memories = await ctx.store.recall({\n query: recallQuery,\n queryEmbedding,\n limit: 12,\n mode: queryEmbedding ? \"auto\" : \"fulltext\",\n });\n log(\"info\", `recall returned ${memories.length} memories`);\n } catch (err) {\n log(\"error\", `recall failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `recall failed: ${err.message}` };\n }\n\n if (!memories.length) {\n log(\"warn\", \"no memories matched — leaving article stale\");\n return { ok: false, reason: \"no source memories matched the article topic\" };\n }\n\n // Step 4: call the LLM.\n let body_md;\n try {\n body_md = await complete(cfg, {\n system: REWRITE_SYSTEM,\n user: buildUserPrompt(article, memories),\n maxTokens: 2048,\n requestModel: cfg.name === \"llamacpp\"\n ? refreshRequestModel(cfg.model, ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined)\n : undefined,\n });\n if (!body_md || !body_md.trim()) throw new Error(\"provider returned empty body\");\n log(\"info\", `completion ok (${body_md.length} chars)`);\n } catch (err) {\n log(\"error\", `LLM completion failed: ${err.message}`, { provider: cfg.name, model: cfg.model, stack: err.stack });\n return { ok: false, reason: `LLM completion failed: ${err.message}` };\n }\n\n // Step 5: extract citations. Refuse to write an article that cites nothing —\n // a zero-citation refresh almost certainly means the model ignored the rules.\n const citedIds = extractCitedMemoryIds(body_md);\n const validIds = new Set(memories.map(m => m.id));\n const grounded = citedIds.filter(id => validIds.has(id));\n if (!grounded.length) {\n log(\"warn\", `regenerated body cites no valid memories — discarding`, {\n citedCount: citedIds.length, validRecallCount: memories.length,\n });\n return { ok: false, reason: \"regenerated body had no valid [[mem:uuid]] citations\" };\n }\n if (grounded.length < citedIds.length) {\n log(\"warn\", `${citedIds.length - grounded.length} citation(s) referenced ids not in recall set — keeping the rest`);\n }\n\n // Step 6: write back via the existing handler (this triggers revision archival via the SQL trigger).\n try {\n const result = await wikiWriteHandler(ctx, {\n slug: article.slug,\n title: article.title,\n summary: article.summary,\n body_md,\n tags: article.tags,\n source_memory_ids: grounded,\n });\n // wikiWriteHandler returns { content: [{ text }] }; surface its text in logs.\n const text = result?.content?.[0]?.text ?? \"\";\n log(\"info\", `regen complete in ${Date.now() - t0}ms — ${text}`);\n return { ok: true, citations: grounded.length, ms: Date.now() - t0 };\n } catch (err) {\n log(\"error\", `wiki_write failed during refresh: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `write failed: ${err.message}` };\n }\n}\n"}, "583aa69ec85927b0f3e0c4433b94db6564a1354c": {"t": 1785748593.2366512, "n": 9995, "h": "1377840530f61995", "c": "// lib/handlers/wiki/regenerate.js\n// Stale-article self-heal. Picks a dedicated, env-configured \"refresh provider\"\n// (see WIKI_REFRESH_PROVIDER in .env.example) and rewrites a stale article in one\n// non-tool-use LLM call. Falls back gracefully if not configured.\n//\n// Why not auto-pick the cheapest provider? Because if the user is on a cloud model\n// by choice, silently firing up the local engine is hostile. The refresh provider\n// must be opt-in via env, just like ROUNDTABLE_AGENTS.\n\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\nimport logger from \"../../helpers/logger.js\";\nimport { ensureLlamaCpp } from \"../../helpers/startLlamaCpp.js\";\nimport { getArticle } from \"./wikiQueries.js\";\nimport { wikiWriteHandler } from \"./wikiHandlers.js\";\n\nconst SUPPORTED = new Set([\"llamacpp\", \"deepseek\", \"anthropic\", \"gemini\"]);\n\n// Parse \"WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\"\n// into { name, model } — or null if unset/malformed.\nexport function parseRefreshProvider(raw) {\n if (!raw || !raw.trim()) return null;\n const [name, ...rest] = raw.trim().split(\":\");\n const model = rest.join(\":\").trim();\n if (!name || !model) {\n logger.warn(`[wiki/refresh] WIKI_REFRESH_PROVIDER=\"${raw}\" — expected \"provider:model\"`);\n return null;\n }\n if (!SUPPORTED.has(name.toLowerCase())) {\n logger.warn(`[wiki/refresh] unsupported refresh provider \"${name}\" — must be one of ${[...SUPPORTED].join(\", \")}`);\n return null;\n }\n return { name: name.toLowerCase(), model };\n}\n\n// One-shot text completion. Returns string. Throws on transport/API errors;\n// callers log and surface a structured failure.\nasync function complete({ name, model }, { system, user, maxTokens = 2048 }) {\n if (name === \"llamacpp\") {\n const base = process.env.LLAMACPP_BASE_URL || \"http://127.0.0.1:8080\";\n const r = await fetch(`${base}/v1/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`llama.cpp HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"deepseek\") {\n if (!process.env.DEEPSEEK_API_KEY) throw new Error(\"DEEPSEEK_API_KEY missing\");\n const r = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${process.env.DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`deepseek HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"anthropic\") {\n if (!process.env.ANTHROPIC_API_KEY) throw new Error(\"ANTHROPIC_API_KEY missing\");\n const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\n const msg = await client.messages.create({\n model,\n system,\n max_tokens: maxTokens,\n messages: [{ role: \"user\", content: user }],\n });\n return msg.content?.find(b => b.type === \"text\")?.text ?? \"\";\n }\n if (name === \"gemini\") {\n if (!process.env.GEMINI_API_KEY) throw new Error(\"GEMINI_API_KEY missing\");\n const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const m = genai.getGenerativeModel({ model, systemInstruction: system });\n const result = await m.generateContent(user);\n return result.response?.text?.() ?? \"\";\n }\n throw new Error(`unknown provider: ${name}`);\n}\n\nconst REWRITE_SYSTEM = `You are rewriting a stale wiki article using a fresh set of source memories.\n\nRULES (non-negotiable):\n- Output ONLY the markdown body. No preamble, no commentary, no code fences around the whole thing.\n- Cite every factual claim with an inline marker of the form [[mem:<uuid>]] using one of the provided memory ids.\n- Do NOT invent facts that aren't in the provided memories.\n- Keep the structure tight — short sections, no filler.\n- Preserve [[other-slug]] sibling links from the prior body where they still make sense.`;\n\nfunction buildUserPrompt(article, memories) {\n const memBlock = memories.length\n ? memories.map(m => `- id: ${m.id}\\n title: ${m.title}\\n content: ${m.content}`).join(\"\\n\\n\")\n : \"(no memories matched — keep the article minimal and flag the gap.)\";\n return [\n `# Article to refresh`,\n `Slug: ${article.slug}`,\n `Title: ${article.title}`,\n article.summary ? `Summary: ${article.summary}` : null,\n ``,\n `## Prior body (now stale)`,\n article.body_md,\n ``,\n `## Available source memories`,\n memBlock,\n ``,\n `Rewrite the body. Cite every claim as [[mem:<uuid>]] using the ids above.`,\n ].filter(Boolean).join(\"\\n\");\n}\n\n// Extract every [[mem:uuid]] marker, dedupe.\nfunction extractCitedMemoryIds(body) {\n const ids = new Set();\n const re = /\\[\\[mem:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\]\\]/gi;\n let m; while ((m = re.exec(body))) ids.add(m[1].toLowerCase());\n return [...ids];\n}\n\n/**\n * Regenerate a stale article in place.\n * Returns { ok: true, revision } on success or { ok: false, reason } on any failure.\n * Never throws — caller can use the return shape to keep serving the stale body.\n */\nexport async function regenerateArticle(ctx, slug) {\n const t0 = Date.now();\n const log = (level, msg, extra = {}) => logger[level](`[wiki/refresh] ${msg}`, { slug, ...extra });\n\n const cfg = parseRefreshProvider(process.env.WIKI_REFRESH_PROVIDER);\n if (!cfg) {\n log(\"warn\", \"WIKI_REFRESH_PROVIDER not set — refresh disabled\");\n return { ok: false, reason: \"refresh provider not configured (set WIKI_REFRESH_PROVIDER)\" };\n }\n\n log(\"info\", `starting regen via ${cfg.name}:${cfg.model}`);\n\n // Step 1: load the article.\n let article;\n try {\n article = await getArticle(ctx.store, slug);\n } catch (err) {\n log(\"error\", `load article failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `load failed: ${err.message}` };\n }\n if (!article) return { ok: false, reason: `article \"${slug}\" not found` };\n\n // Step 2: if the provider is the local engine and auto-start is on, ensure it's running.\n if (cfg.name === \"llamacpp\" && process.env.WIKI_REFRESH_AUTOSTART_LLAMACPP === \"true\") {\n try { await ensureLlamaCpp(); }\n catch (err) {\n log(\"error\", `ensureLlamaCpp failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `llama.cpp auto-start failed: ${err.message}` };\n }\n }\n\n // Step 3: gather fresh source memories.\n const recallQuery = [article.title, article.summary].filter(Boolean).join(\". \");\n let memories;\n try {\n const queryEmbedding = await ctx.generateEmbedding(recallQuery);\n memories = await ctx.store.recall({\n query: recallQuery,\n queryEmbedding,\n limit: 12,\n mode: queryEmbedding ? \"auto\" : \"fulltext\",\n });\n log(\"info\", `recall returned ${memories.length} memories`);\n } catch (err) {\n log(\"error\", `recall failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `recall failed: ${err.message}` };\n }\n\n if (!memories.length) {\n log(\"warn\", \"no memories matched — leaving article stale\");\n return { ok: false, reason: \"no source memories matched the article topic\" };\n }\n\n // Step 4: call the LLM.\n let body_md;\n try {\n body_md = await complete(cfg, {\n system: REWRITE_SYSTEM,\n user: buildUserPrompt(article, memories),\n maxTokens: 2048,\n });\n if (!body_md || !body_md.trim()) throw new Error(\"provider returned empty body\");\n log(\"info\", `completion ok (${body_md.length} chars)`);\n } catch (err) {\n log(\"error\", `LLM completion failed: ${err.message}`, { provider: cfg.name, model: cfg.model, stack: err.stack });\n return { ok: false, reason: `LLM completion failed: ${err.message}` };\n }\n\n // Step 5: extract citations. Refuse to write an article that cites nothing —\n // a zero-citation refresh almost certainly means the model ignored the rules.\n const citedIds = extractCitedMemoryIds(body_md);\n const validIds = new Set(memories.map(m => m.id));\n const grounded = citedIds.filter(id => validIds.has(id));\n if (!grounded.length) {\n log(\"warn\", `regenerated body cites no valid memories — discarding`, {\n citedCount: citedIds.length, validRecallCount: memories.length,\n });\n return { ok: false, reason: \"regenerated body had no valid [[mem:uuid]] citations\" };\n }\n if (grounded.length < citedIds.length) {\n log(\"warn\", `${citedIds.length - grounded.length} citation(s) referenced ids not in recall set — keeping the rest`);\n }\n\n // Step 6: write back via the existing handler (this triggers revision archival via the SQL trigger).\n try {\n const result = await wikiWriteHandler(ctx, {\n slug: article.slug,\n title: article.title,\n summary: article.summary,\n body_md,\n tags: article.tags,\n source_memory_ids: grounded,\n });\n // wikiWriteHandler returns { content: [{ text }] }; surface its text in logs.\n const text = result?.content?.[0]?.text ?? \"\";\n log(\"info\", `regen complete in ${Date.now() - t0}ms — ${text}`);\n return { ok: true, citations: grounded.length, ms: Date.now() - t0 };\n } catch (err) {\n log(\"error\", `wiki_write failed during refresh: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `write failed: ${err.message}` };\n }\n}\n"}, "99025ad9d612cf9c08eb848aec54fcd5fd067ada": {"t": 1785748595.3987024, "n": 9919, "h": "d20f41dc950c45ba", "c": "// lib/handlers/wiki/regenerate.js\n// Stale-article self-heal. Picks a dedicated, env-configured \"refresh provider\"\n// (see WIKI_REFRESH_PROVIDER in .env.example) and rewrites a stale article in one\n// non-tool-use LLM call. Falls back gracefully if not configured.\n//\n// Why not auto-pick the cheapest provider? Because if the user is on a cloud model\n// by choice, silently firing up Ollama is hostile. The refresh provider must be\n// opt-in via env, just like ROUNDTABLE_AGENTS.\n\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\nimport logger from \"../../helpers/logger.js\";\nimport { ensureOllama } from \"../../helpers/startOllama.js\";\nimport { getArticle } from \"./wikiQueries.js\";\nimport { wikiWriteHandler } from \"./wikiHandlers.js\";\n\nconst SUPPORTED = new Set([\"ollama\", \"deepseek\", \"anthropic\", \"gemini\"]);\n\n// Parse \"WIKI_REFRESH_PROVIDER=ollama:llama3.1\" into { name, model } — or null if unset/malformed.\nexport function parseRefreshProvider(raw) {\n if (!raw || !raw.trim()) return null;\n const [name, ...rest] = raw.trim().split(\":\");\n const model = rest.join(\":\").trim();\n if (!name || !model) {\n logger.warn(`[wiki/refresh] WIKI_REFRESH_PROVIDER=\"${raw}\" — expected \"provider:model\"`);\n return null;\n }\n if (!SUPPORTED.has(name.toLowerCase())) {\n logger.warn(`[wiki/refresh] unsupported refresh provider \"${name}\" — must be one of ${[...SUPPORTED].join(\", \")}`);\n return null;\n }\n return { name: name.toLowerCase(), model };\n}\n\n// One-shot text completion. Returns string. Throws on transport/API errors;\n// callers log and surface a structured failure.\nasync function complete({ name, model }, { system, user, maxTokens = 2048 }) {\n if (name === \"ollama\") {\n const base = process.env.OLLAMA_BASE_URL || \"http://localhost:11434\";\n const r = await fetch(`${base}/v1/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`ollama HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"deepseek\") {\n if (!process.env.DEEPSEEK_API_KEY) throw new Error(\"DEEPSEEK_API_KEY missing\");\n const r = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${process.env.DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`deepseek HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"anthropic\") {\n if (!process.env.ANTHROPIC_API_KEY) throw new Error(\"ANTHROPIC_API_KEY missing\");\n const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\n const msg = await client.messages.create({\n model,\n system,\n max_tokens: maxTokens,\n messages: [{ role: \"user\", content: user }],\n });\n return msg.content?.find(b => b.type === \"text\")?.text ?? \"\";\n }\n if (name === \"gemini\") {\n if (!process.env.GEMINI_API_KEY) throw new Error(\"GEMINI_API_KEY missing\");\n const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const m = genai.getGenerativeModel({ model, systemInstruction: system });\n const result = await m.generateContent(user);\n return result.response?.text?.() ?? \"\";\n }\n throw new Error(`unknown provider: ${name}`);\n}\n\nconst REWRITE_SYSTEM = `You are rewriting a stale wiki article using a fresh set of source memories.\n\nRULES (non-negotiable):\n- Output ONLY the markdown body. No preamble, no commentary, no code fences around the whole thing.\n- Cite every factual claim with an inline marker of the form [[mem:<uuid>]] using one of the provided memory ids.\n- Do NOT invent facts that aren't in the provided memories.\n- Keep the structure tight — short sections, no filler.\n- Preserve [[other-slug]] sibling links from the prior body where they still make sense.`;\n\nfunction buildUserPrompt(article, memories) {\n const memBlock = memories.length\n ? memories.map(m => `- id: ${m.id}\\n title: ${m.title}\\n content: ${m.content}`).join(\"\\n\\n\")\n : \"(no memories matched — keep the article minimal and flag the gap.)\";\n return [\n `# Article to refresh`,\n `Slug: ${article.slug}`,\n `Title: ${article.title}`,\n article.summary ? `Summary: ${article.summary}` : null,\n ``,\n `## Prior body (now stale)`,\n article.body_md,\n ``,\n `## Available source memories`,\n memBlock,\n ``,\n `Rewrite the body. Cite every claim as [[mem:<uuid>]] using the ids above.`,\n ].filter(Boolean).join(\"\\n\");\n}\n\n// Extract every [[mem:uuid]] marker, dedupe.\nfunction extractCitedMemoryIds(body) {\n const ids = new Set();\n const re = /\\[\\[mem:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\]\\]/gi;\n let m; while ((m = re.exec(body))) ids.add(m[1].toLowerCase());\n return [...ids];\n}\n\n/**\n * Regenerate a stale article in place.\n * Returns { ok: true, revision } on success or { ok: false, reason } on any failure.\n * Never throws — caller can use the return shape to keep serving the stale body.\n */\nexport async function regenerateArticle(ctx, slug) {\n const t0 = Date.now();\n const log = (level, msg, extra = {}) => logger[level](`[wiki/refresh] ${msg}`, { slug, ...extra });\n\n const cfg = parseRefreshProvider(process.env.WIKI_REFRESH_PROVIDER);\n if (!cfg) {\n log(\"warn\", \"WIKI_REFRESH_PROVIDER not set — refresh disabled\");\n return { ok: false, reason: \"refresh provider not configured (set WIKI_REFRESH_PROVIDER)\" };\n }\n\n log(\"info\", `starting regen via ${cfg.name}:${cfg.model}`);\n\n // Step 1: load the article.\n let article;\n try {\n article = await getArticle(ctx.store, slug);\n } catch (err) {\n log(\"error\", `load article failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `load failed: ${err.message}` };\n }\n if (!article) return { ok: false, reason: `article \"${slug}\" not found` };\n\n // Step 2: if the provider is ollama and auto-start is on, ensure it's running.\n if (cfg.name === \"ollama\" && process.env.WIKI_REFRESH_AUTOSTART_OLLAMA === \"true\") {\n try { await ensureOllama(); }\n catch (err) {\n log(\"error\", `ensureOllama failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `ollama auto-start failed: ${err.message}` };\n }\n }\n\n // Step 3: gather fresh source memories.\n const recallQuery = [article.title, article.summary].filter(Boolean).join(\". \");\n let memories;\n try {\n const queryEmbedding = await ctx.generateEmbedding(recallQuery);\n memories = await ctx.store.recall({\n query: recallQuery,\n queryEmbedding,\n limit: 12,\n mode: queryEmbedding ? \"auto\" : \"fulltext\",\n });\n log(\"info\", `recall returned ${memories.length} memories`);\n } catch (err) {\n log(\"error\", `recall failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `recall failed: ${err.message}` };\n }\n\n if (!memories.length) {\n log(\"warn\", \"no memories matched — leaving article stale\");\n return { ok: false, reason: \"no source memories matched the article topic\" };\n }\n\n // Step 4: call the LLM.\n let body_md;\n try {\n body_md = await complete(cfg, {\n system: REWRITE_SYSTEM,\n user: buildUserPrompt(article, memories),\n maxTokens: 2048,\n });\n if (!body_md || !body_md.trim()) throw new Error(\"provider returned empty body\");\n log(\"info\", `completion ok (${body_md.length} chars)`);\n } catch (err) {\n log(\"error\", `LLM completion failed: ${err.message}`, { provider: cfg.name, model: cfg.model, stack: err.stack });\n return { ok: false, reason: `LLM completion failed: ${err.message}` };\n }\n\n // Step 5: extract citations. Refuse to write an article that cites nothing —\n // a zero-citation refresh almost certainly means the model ignored the rules.\n const citedIds = extractCitedMemoryIds(body_md);\n const validIds = new Set(memories.map(m => m.id));\n const grounded = citedIds.filter(id => validIds.has(id));\n if (!grounded.length) {\n log(\"warn\", `regenerated body cites no valid memories — discarding`, {\n citedCount: citedIds.length, validRecallCount: memories.length,\n });\n return { ok: false, reason: \"regenerated body had no valid [[mem:uuid]] citations\" };\n }\n if (grounded.length < citedIds.length) {\n log(\"warn\", `${citedIds.length - grounded.length} citation(s) referenced ids not in recall set — keeping the rest`);\n }\n\n // Step 6: write back via the existing handler (this triggers revision archival via the SQL trigger).\n try {\n const result = await wikiWriteHandler(ctx, {\n slug: article.slug,\n title: article.title,\n summary: article.summary,\n body_md,\n tags: article.tags,\n source_memory_ids: grounded,\n });\n // wikiWriteHandler returns { content: [{ text }] }; surface its text in logs.\n const text = result?.content?.[0]?.text ?? \"\";\n log(\"info\", `regen complete in ${Date.now() - t0}ms — ${text}`);\n return { ok: true, citations: grounded.length, ms: Date.now() - t0 };\n } catch (err) {\n log(\"error\", `wiki_write failed during refresh: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `write failed: ${err.message}` };\n }\n}\n"}, "2b4d143ca069ef7b115bf38e8e1cdfba32acc8de": {"t": 1786067872.7475753, "n": 12109, "h": "cc5d86b1e7521882", "c": "// lib/handlers/wiki/regenerate.js\n// Stale-article self-heal. Picks a dedicated, env-configured \"refresh provider\"\n// (see WIKI_REFRESH_PROVIDER in .env.example) and rewrites a stale article in one\n// non-tool-use LLM call. Falls back gracefully if not configured.\n//\n// Why not auto-pick the cheapest provider? Because if the user is on a cloud model\n// by choice, silently firing up the local engine is hostile. The refresh provider\n// must be opt-in via env, just like ROUNDTABLE_AGENTS.\n\nimport Anthropic from \"@anthropic-ai/sdk\";\nimport { GoogleGenerativeAI } from \"@google/generative-ai\";\nimport logger from \"../../helpers/logger.js\";\nimport { ensureLlamaCpp, getLoadedModels } from \"../../helpers/startLlamaCpp.js\";\nimport { LLAMACPP_MAIN_ALIAS } from \"../../helpers/llamacppAliases.js\";\nimport { resolveProvider } from \"../../providers/index.js\";\nimport { getArticle } from \"./wikiQueries.js\";\nimport { wikiWriteHandler } from \"./wikiHandlers.js\";\nimport { isVectorSearchable } from \"../../helpers/vecMeta.js\";\n\nconst SUPPORTED = new Set([\"llamacpp\", \"deepseek\", \"anthropic\", \"gemini\"]);\n\n// Parse \"WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\"\n// into { name, model } — or null if unset/malformed.\nexport function parseRefreshProvider(raw) {\n if (!raw || !raw.trim()) return null;\n const [name, ...rest] = raw.trim().split(\":\");\n const model = rest.join(\":\").trim();\n if (!name || !model) {\n logger.warn(`[wiki/refresh] WIKI_REFRESH_PROVIDER=\"${raw}\" — expected \"provider:model\"`);\n return null;\n }\n if (!SUPPORTED.has(name.toLowerCase())) {\n logger.warn(`[wiki/refresh] unsupported refresh provider \"${name}\" — must be one of ${[...SUPPORTED].join(\", \")}`);\n return null;\n }\n return { name: name.toLowerCase(), model };\n}\n\nexport function refreshRequestModel(model, mainProvider = resolveProvider({ name: \"llamacpp\" })) {\n return model === mainProvider?.model\n ? (mainProvider.requestModel || LLAMACPP_MAIN_ALIAS)\n : model;\n}\n\nexport async function checkLlamaCppModelServed(model, requestModel = model) {\n const payload = await getLoadedModels();\n if (!payload) return null;\n const served = new Set();\n for (const entry of payload.data ?? []) {\n if (entry?.id) served.add(entry.id);\n for (const alias of entry?.aliases ?? []) served.add(alias);\n }\n if (served.has(requestModel)) return null;\n const loaded = [...served].join(\", \") || \"none\";\n return `model not served: ${model}; loaded: ${loaded}. Restart Aperio to pick up the preset.`;\n}\n\n// One-shot text completion. Returns string. Throws on transport/API errors;\n// callers log and surface a structured failure.\nasync function complete({ name, model }, { system, user, maxTokens = 2048, requestModel }) {\n if (name === \"llamacpp\") {\n const base = process.env.LLAMACPP_BASE_URL || \"http://127.0.0.1:8080\";\n // Route through the resident preset alias when the configured refresh model\n // IS the main model: requesting the raw HF id makes llama.cpp's router load\n // a second, full-context copy (see lib/helpers/completion.js). A genuinely\n // different refresh model is honored as-is — the user opted into a 2nd load.\n requestModel ??= refreshRequestModel(model);\n const r = await fetch(`${base}/v1/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: requestModel,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`llama.cpp HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"deepseek\") {\n if (!process.env.DEEPSEEK_API_KEY) throw new Error(\"DEEPSEEK_API_KEY missing\");\n const r = await fetch(\"https://api.deepseek.com/v1/chat/completions\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${process.env.DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [{ role: \"system\", content: system }, { role: \"user\", content: user }],\n stream: false,\n max_tokens: maxTokens,\n }),\n signal: AbortSignal.timeout(120_000),\n });\n if (!r.ok) throw new Error(`deepseek HTTP ${r.status}: ${(await r.text()).slice(0, 200)}`);\n const data = await r.json();\n return data.choices?.[0]?.message?.content ?? \"\";\n }\n if (name === \"anthropic\") {\n if (!process.env.ANTHROPIC_API_KEY) throw new Error(\"ANTHROPIC_API_KEY missing\");\n const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\n const msg = await client.messages.create({\n model,\n system,\n max_tokens: maxTokens,\n messages: [{ role: \"user\", content: user }],\n });\n return msg.content?.find(b => b.type === \"text\")?.text ?? \"\";\n }\n if (name === \"gemini\") {\n if (!process.env.GEMINI_API_KEY) throw new Error(\"GEMINI_API_KEY missing\");\n const genai = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);\n const m = genai.getGenerativeModel({ model, systemInstruction: system });\n const result = await m.generateContent(user);\n return result.response?.text?.() ?? \"\";\n }\n throw new Error(`unknown provider: ${name}`);\n}\n\nconst REWRITE_SYSTEM = `You are rewriting a stale wiki article using a fresh set of source memories.\n\nRULES (non-negotiable):\n- Output ONLY the markdown body. No preamble, no commentary, no code fences around the whole thing.\n- Cite every factual claim with an inline marker of the form [[mem:<uuid>]] using one of the provided memory ids.\n- Do NOT invent facts that aren't in the provided memories.\n- Keep the structure tight — short sections, no filler.\n- Preserve [[other-slug]] sibling links from the prior body where they still make sense.`;\n\nexport function buildUserPrompt(article, memories) {\n const memBlock = memories.length\n ? memories.map(m => `- id: ${m.id}\\n title: ${m.title}\\n content: ${m.content}`).join(\"\\n\\n\")\n : \"(no memories matched — keep the article minimal and flag the gap.)\";\n return [\n `# Article to refresh`,\n `Slug: ${article.slug}`,\n `Title: ${article.title}`,\n article.summary ? `Summary: ${article.summary}` : null,\n ``,\n `## Prior body (now stale)`,\n article.body_md,\n ``,\n `## Available source memories`,\n memBlock,\n ``,\n `Rewrite the body. Cite every claim as [[mem:<uuid>]] using the ids above.`,\n ].filter(Boolean).join(\"\\n\");\n}\n\n// Extract every [[mem:uuid]] marker, dedupe.\nexport function extractCitedMemoryIds(body) {\n const ids = new Set();\n const re = /\\[\\[mem:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\]\\]/gi;\n let m; while ((m = re.exec(body))) ids.add(m[1].toLowerCase());\n return [...ids];\n}\n\n/**\n * Regenerate a stale article in place.\n * Returns { ok: true, revision } on success or { ok: false, reason } on any failure.\n * Never throws — caller can use the return shape to keep serving the stale body.\n */\nexport async function regenerateArticle(ctx, slug) {\n const t0 = Date.now();\n const log = (level, msg, extra = {}) => logger[level](`[wiki/refresh] ${msg}`, { slug, ...extra });\n\n const cfg = parseRefreshProvider(process.env.WIKI_REFRESH_PROVIDER);\n if (!cfg) {\n log(\"warn\", \"WIKI_REFRESH_PROVIDER not set — refresh disabled\");\n return { ok: false, reason: \"refresh provider not configured (set WIKI_REFRESH_PROVIDER)\" };\n }\n\n log(\"info\", `starting regen via ${cfg.name}:${cfg.model}`);\n\n // Step 1: load the article.\n let article;\n try {\n article = await getArticle(ctx.store, slug);\n } catch (err) {\n log(\"error\", `load article failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `load failed: ${err.message}` };\n }\n if (!article) return { ok: false, reason: `article \"${slug}\" not found` };\n\n // Step 2: if the provider is the local engine and auto-start is on, ensure it's running.\n if (cfg.name === \"llamacpp\" && process.env.WIKI_REFRESH_AUTOSTART_LLAMACPP === \"true\") {\n try { await ensureLlamaCpp(); }\n catch (err) {\n log(\"error\", `ensureLlamaCpp failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `llama.cpp auto-start failed: ${err.message}` };\n }\n }\n\n if (cfg.name === \"llamacpp\") {\n const requestModel = refreshRequestModel(\n cfg.model,\n ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined,\n );\n const availabilityError = await checkLlamaCppModelServed(cfg.model, requestModel);\n if (availabilityError) {\n log(\"error\", availabilityError, { provider: cfg.name, model: cfg.model });\n return { ok: false, reason: availabilityError };\n }\n }\n\n // Step 3: gather fresh source memories.\n const recallQuery = [article.title, article.summary].filter(Boolean).join(\". \");\n let memories;\n try {\n // FTS-only while the memories store is stale/reindexing (issue #287).\n const queryEmbedding = await isVectorSearchable(ctx.store, \"memories\")\n ? await ctx.generateEmbedding(recallQuery)\n : null;\n memories = await ctx.store.recall({\n query: recallQuery,\n queryEmbedding,\n limit: 12,\n mode: queryEmbedding ? \"auto\" : \"fulltext\",\n });\n log(\"info\", `recall returned ${memories.length} memories`);\n } catch (err) {\n log(\"error\", `recall failed: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `recall failed: ${err.message}` };\n }\n\n if (!memories.length) {\n log(\"warn\", \"no memories matched — leaving article stale\");\n return { ok: false, reason: \"no source memories matched the article topic\" };\n }\n\n // Step 4: call the LLM.\n let body_md;\n try {\n body_md = await complete(cfg, {\n system: REWRITE_SYSTEM,\n user: buildUserPrompt(article, memories),\n maxTokens: 2048,\n requestModel: cfg.name === \"llamacpp\"\n ? refreshRequestModel(cfg.model, ctx?.provider?.name === \"llamacpp\" ? ctx.provider : undefined)\n : undefined,\n });\n if (!body_md || !body_md.trim()) throw new Error(\"provider returned empty body\");\n log(\"info\", `completion ok (${body_md.length} chars)`);\n } catch (err) {\n log(\"error\", `LLM completion failed: ${err.message}`, { provider: cfg.name, model: cfg.model, stack: err.stack });\n return { ok: false, reason: `LLM completion failed: ${err.message}` };\n }\n\n // Step 5: extract citations. Refuse to write an article that cites nothing —\n // a zero-citation refresh almost certainly means the model ignored the rules.\n const citedIds = extractCitedMemoryIds(body_md);\n const validIds = new Set(memories.map(m => m.id));\n const grounded = citedIds.filter(id => validIds.has(id));\n if (!grounded.length) {\n log(\"warn\", `regenerated body cites no valid memories — discarding`, {\n citedCount: citedIds.length, validRecallCount: memories.length,\n });\n return { ok: false, reason: \"regenerated body had no valid [[mem:uuid]] citations\" };\n }\n if (grounded.length < citedIds.length) {\n log(\"warn\", `${citedIds.length - grounded.length} citation(s) referenced ids not in recall set — keeping the rest`);\n }\n\n // Step 6: write back via the existing handler (this triggers revision archival via the SQL trigger).\n try {\n const result = await wikiWriteHandler(ctx, {\n slug: article.slug,\n title: article.title,\n summary: article.summary,\n body_md,\n tags: article.tags,\n source_memory_ids: grounded,\n });\n // wikiWriteHandler returns { content: [{ text }] }; surface its text in logs.\n const text = result?.content?.[0]?.text ?? \"\";\n log(\"info\", `regen complete in ${Date.now() - t0}ms — ${text}`);\n return { ok: true, citations: grounded.length, ms: Date.now() - t0 };\n } catch (err) {\n log(\"error\", `wiki_write failed during refresh: ${err.message}`, { stack: err.stack });\n return { ok: false, reason: `write failed: ${err.message}` };\n }\n}\n"}}, ".env.example": {"2b2a200280e92e939fc11bf60927fdf887f58cf7": {"t": 1785747076.801656, "n": 25054, "h": "d54945eb5e2c0c4a", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (122 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Working directory for the claude-code provider's CLI subprocess (AI_PROVIDER=claude-code). Left unset, the subprocess inherits the server process's own cwd — the repo root for a normal Aperio install, which is the intended behavior. Set only to sandbox that subprocess away from the real project tree (e.g. an isolated test harness).\n# APERIO_CLAUDE_CODE_CWD=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# Private directory containing Aperio's pinned llama-server binary. Docker points this into its persistent runtime volume; normal installs keep it inside the app folder.\n# LLAMACPP_VENDOR_DIR=vendor/llamacpp\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# JSON object keyed by HF repo path (with optional :quant suffix) overriding both cached GGUF inspection and the curated model_facts database catalog. Each value has { sizeGB, maxContext, kvBytesPerToken, architecture?, activeParams? }. Resolution order: configured override → cached GGUF → model_facts catalog → conservative generic facts.\n# APERIO_MODEL_FACTS_OVERRIDES=\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Minimum served context for Aperio's default Gemma 4 E2B local agent on machines with at least 7.5 GiB RAM. Prevents the identity, memory pointer, and minimum tool schema from exceeding a tiny llama.cpp window.\n# LLAMACPP_MIN_AGENT_CTX=8192\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# Auto-promote high-confidence docgraph facts (amount_due/grand_total + due_date/invoice_date/service_period) into the memory store for trend-question recall. Off unless set to exactly 'on'.\n# DOCGRAPH_AUTO_MEMORY=on\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "3aa9255b06a0de4d1d50587e241664603545b64f": {"t": 1785748587.416294, "n": 25054, "h": "d54945eb5e2c0c4a", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (122 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Working directory for the claude-code provider's CLI subprocess (AI_PROVIDER=claude-code). Left unset, the subprocess inherits the server process's own cwd — the repo root for a normal Aperio install, which is the intended behavior. Set only to sandbox that subprocess away from the real project tree (e.g. an isolated test harness).\n# APERIO_CLAUDE_CODE_CWD=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# Private directory containing Aperio's pinned llama-server binary. Docker points this into its persistent runtime volume; normal installs keep it inside the app folder.\n# LLAMACPP_VENDOR_DIR=vendor/llamacpp\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# JSON object keyed by HF repo path (with optional :quant suffix) overriding both cached GGUF inspection and the curated model_facts database catalog. Each value has { sizeGB, maxContext, kvBytesPerToken, architecture?, activeParams? }. Resolution order: configured override → cached GGUF → model_facts catalog → conservative generic facts.\n# APERIO_MODEL_FACTS_OVERRIDES=\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Minimum served context for Aperio's default Gemma 4 E2B local agent on machines with at least 7.5 GiB RAM. Prevents the identity, memory pointer, and minimum tool schema from exceeding a tiny llama.cpp window.\n# LLAMACPP_MIN_AGENT_CTX=8192\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# Auto-promote high-confidence docgraph facts (amount_due/grand_total + due_date/invoice_date/service_period) into the memory store for trend-question recall. Off unless set to exactly 'on'.\n# DOCGRAPH_AUTO_MEMORY=on\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "bbe83d4c6fd39bd5a01421ddc4e03e883e2e392b": {"t": 1785748588.641958, "n": 24593, "h": "b2ecab48cf6f0a2c", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (120 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Working directory for the claude-code provider's CLI subprocess (AI_PROVIDER=claude-code). Left unset, the subprocess inherits the server process's own cwd — the repo root for a normal Aperio install, which is the intended behavior. Set only to sandbox that subprocess away from the real project tree (e.g. an isolated test harness).\n# APERIO_CLAUDE_CODE_CWD=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# JSON object keyed by HF repo path (with optional :quant suffix) overriding both cached GGUF inspection and the curated model_facts database catalog. Each value has { sizeGB, maxContext, kvBytesPerToken, architecture?, activeParams? }. Resolution order: configured override → cached GGUF → model_facts catalog → conservative generic facts.\n# APERIO_MODEL_FACTS_OVERRIDES=\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# Auto-promote high-confidence docgraph facts (amount_due/grand_total + due_date/invoice_date/service_period) into the memory store for trend-question recall. Off unless set to exactly 'on'.\n# DOCGRAPH_AUTO_MEMORY=on\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "01c545cf284406ae64226b70d6facaa1f111c089": {"t": 1785748591.462188, "n": 24553, "h": "56167dde6c18f1de", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (120 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Working directory for the claude-code provider's CLI subprocess (AI_PROVIDER=claude-code). Left unset, the subprocess inherits the server process's own cwd — the repo root for a normal Aperio install, which is the intended behavior. Set only to sandbox that subprocess away from the real project tree (e.g. an isolated test harness).\n# APERIO_CLAUDE_CODE_CWD=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# JSON object keyed by HF repo path (with optional :quant suffix) overriding the curated MODEL_FACTS dict for uncached or custom models. Each value has { sizeGB, maxContext, kvBytesPerToken, architecture?, activeParams? }. Slotted between GGUF inspection and the curated catalog in resolveModelFacts.\n# APERIO_MODEL_FACTS_OVERRIDES=\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# Auto-promote high-confidence docgraph facts (amount_due/grand_total + due_date/invoice_date/service_period) into the memory store for trend-question recall. Off unless set to exactly 'on'.\n# DOCGRAPH_AUTO_MEMORY=on\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "9f31ed68f44b115f20390fab3a2edb1e46ddd746": {"t": 1785748593.7541711, "n": 24190, "h": "071afdf21b8eb2bc", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (119 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# JSON object keyed by HF repo path (with optional :quant suffix) overriding the curated MODEL_FACTS dict for uncached or custom models. Each value has { sizeGB, maxContext, kvBytesPerToken, architecture?, activeParams? }. Slotted between GGUF inspection and the curated catalog in resolveModelFacts.\n# APERIO_MODEL_FACTS_OVERRIDES=\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# Auto-promote high-confidence docgraph facts (amount_due/grand_total + due_date/invoice_date/service_period) into the memory store for trend-question recall. Off unless set to exactly 'on'.\n# DOCGRAPH_AUTO_MEMORY=on\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "aa890b89666e6aa0f8e9b104491255aa7c0f554a": {"t": 1785748595.0397887, "n": 23972, "h": "76f95b3c24107e8e", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (118 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# JSON object keyed by HF repo path (with optional :quant suffix) overriding the curated MODEL_FACTS dict for uncached or custom models. Each value has { sizeGB, maxContext, kvBytesPerToken, architecture?, activeParams? }. Slotted between GGUF inspection and the curated catalog in resolveModelFacts.\n# APERIO_MODEL_FACTS_OVERRIDES=\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "03728a3e1177b87afff064a4c602d1ed119ac5ac": {"t": 1785748597.3709412, "n": 23654, "h": "426888182244a556", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (117 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\nAPERIO_CAPABLE_MODELS=unsloth/gemma-4-E2B-it-qat-GGUF:Q4_K_XL\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\nLLAMACPP_MODEL=unsloth/gemma-4-E2B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "b98082e0639c994268f7b1381aa5b774723a486a": {"t": 1785748598.5972342, "n": 23639, "h": "b02de091c5b424a9", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (117 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\nLLAMACPP_MODEL=unsloth/gemma-4-E2B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "1363d8847fedc579e3bd7d77c873c012a105a335": {"t": 1785748601.8854187, "n": 23638, "h": "d97ca36b5f918763", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (117 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "31bad19ba9c0af82f1de6826cbef649ee67efae0": {"t": 1785748603.1021974, "n": 23431, "h": "fdeb842a7c95d00f", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (116 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum Codex tool/action items allowed in one turn. Set to 0 to disable this guardrail.\n# CODEX_TURN_MAX_TOOL_CALLS=32\n\n# Maximum aggregate processed input tokens reported for one Codex turn. Because the CLI reports this at turn completion, enforcement occurs when usage arrives. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=250000\n\n# Maximum wall-clock seconds for one Codex turn. Set to 0 to disable this guardrail.\n# CODEX_TURN_MAX_SECONDS=900\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "358fbf5d49f7365858725972da20d6feaae4b79e": {"t": 1785748604.331338, "n": 22967, "h": "8a8828281c767c00", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (113 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "3fdcac0ce8c42b9de0fe53e1319c0f31d6b4cdb9": {"t": 1785748605.5503373, "n": 22588, "h": "7fe56f8ef16cd72c", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (112 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "1eb9f5e8d2d822ad76517a8dcbd95581337cd6a7": {"t": 1785748606.7795572, "n": 21982, "h": "73026f12de3eda6c", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (110 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "bf4c801f5d763d2a618561ac1f6bffaaaea5c3c4": {"t": 1785748608.0836504, "n": 25044, "h": "219690064de3540e", "c": "# ============================================================\n# Aperio — configuration bootstrap\n# AUTO-GENERATED from lib/config.js — run `npm run gen:env` to regenerate;\n# edit the registry, not this file. NEVER commit a real .env to git.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch. Only the START HERE block is normally edited.\n#\n# Inventory: 111 configurable variables are recorded in this file.\n# To cross-check it against the registry, run `npm run gen:env:check`.\n# If it reports that this file is stale, run `npm run gen:env` to regenerate it.\n#\n# To configure by hand: copy this file to `.env`, edit START HERE, and run\n# `npm start`. Most people never need to go past the STOP line.\n#\n# Precedence: .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default\n# below). Set it to db when the Settings UI should override this file.\n#\n# Full generated reference: docs/config-reference.md\n# Copy to `.env` and edit — or just run `npm start` and take it from there.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use the local llama.cpp engine), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\nDEEPSEEK_MODEL=deepseek-v4-flash\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\nGEMINI_MODEL=gemini-2.0-flash\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n# Port the web app runs on.\nPORT=31337\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=db) a value saved in the UI wins; set it to env to make what you write here win instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local llama.cpp models are lean chat models by default; list trusted ones to grant them tools + memory.\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Defaults shown — override only if your llama-server setup differs.\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Privacy ───────────────────────────────────────────────────\n# How sensitive memories are handled when a cloud provider is active.\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n# POSTGRES_HOST=localhost\n# POSTGRES_PORT=5432\n# POSTGRES_DB=aperio\n# POSTGRES_USER=aperio\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}, "cc67052ce8b607cbbfa9eab0f88c6e4c7b9131cf": {"t": 1785748609.1512356, "n": 2285, "h": "b799946b58e805a3", "c": "# ============================================================\n# Aperio — configuration bootstrap\n# AUTO-GENERATED from lib/config.js — run `npm run gen:env` to regenerate;\n# edit the registry, not this file. NEVER commit a real .env to git.\n#\n# Only what belongs in .env lives here: the START-HERE provider block and\n# the bootstrap/security keys read before the database opens. Everything\n# else is set in the app's Settings — full reference: docs/config-reference.md\n# Copy to `.env` and edit — or just run `npm start` and take it from there.\n# ============================================================\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\nDEEPSEEK_MODEL=deepseek-v4-flash\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\nGEMINI_MODEL=gemini-2.0-flash\n# Port the web app runs on.\nPORT=31337\n\n# ╶───────────────────────────────────────────────────────────╴\n# Everything else is configured in the app's Settings (saved to the\n# database). Any variable from docs/config-reference.md still works\n# here — set APERIO_CONFIG_PRECEDENCE=env to make this file win.\n# ╶───────────────────────────────────────────────────────────╴\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# DB_BACKEND=sqlite\n# SQLITE_PATH=./.sqlite/aperio.db\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\nPOSTGRES_HOST=localhost\nPOSTGRES_PORT=5432\nPOSTGRES_DB=aperio\nPOSTGRES_USER=aperio\nPOSTGRES_PASSWORD=aperio_secret\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# HOST=0.0.0.0\n\n# ── Network security ──────────────────────────────────────────\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n# APERIO_CSP=on\n# APERIO_AUTH_TOKEN=\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# APERIO_TLS_CERT=/path/to/cert.pem\n# APERIO_TLS_KEY=/path/to/key.pem\n# APERIO_SESSION_KEY=\n"}, "af0640141e4fc046b6d15030d6f72b53c23ae2b9": {"t": 1785748610.3758147, "n": 24428, "h": "36755cc32f33c027", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use the local llama.cpp engine), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ���═══════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local llama.cpp models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Defaults shown — override only if your llama-server setup differs.\n\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\nWIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "12b1a564d9a459728c18717228f88a89a75f4efd": {"t": 1785748611.6205146, "n": 24201, "h": "ce354a3cff770426", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use the local llama.cpp engine), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local llama.cpp models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Defaults shown — override only if your llama-server setup differs.\n\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E4B-it-qat-GGUF:Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\nWIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "704da9219ae86d7c642f3230e6e64f7e40e1b302": {"t": 1785748612.85931, "n": 23270, "h": "53a1ccbd5b67ca2f", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use the local llama.cpp engine), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local llama.cpp models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Defaults shown — override only if your llama-server setup differs.\n\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Also used as the router-preset model name and the `model` field sent on /v1/chat/completions.\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\nWIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "930b3e8b8a4eb36f3993c3c2b6801b928e9a7aae": {"t": 1785748614.125097, "n": 23128, "h": "3477f13e80a8a5e9", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use the local llama.cpp engine), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local llama.cpp models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Defaults shown — override only if your llama-server setup differs.\n\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Also used as the router-preset model name and the `model` field sent on /v1/chat/completions.\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\nWIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "583aa69ec85927b0f3e0c4433b94db6564a1354c": {"t": 1785748615.5139163, "n": 22932, "h": "6d21acfb7666ebc4", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use the local llama.cpp engine), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local llama.cpp models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Defaults shown — override only if your llama-server setup differs.\n\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Also used as the router-preset model name and the `model` field sent on /v1/chat/completions.\n# LLAMACPP_MODEL=Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Where llama-server caches downloaded GGUF weights (Hugging Face hub cache layout). Shared by the vendored server and the setup wizard's model-presence check, so both agree on one location.\n# LLAMA_CACHE=./var/models\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\nWIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "f7da2efe41ae67cb7e86ad559e6aba7c42b0eb4b": {"t": 1785748616.7580662, "n": 21764, "h": "ac494a191f029039", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | ollama | claude-code | codex\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to local Ollama + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (OLLAMA_VLM_MODEL).\n# OLLAMA_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local Ollama provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "5e9f0fd9ff2a3c32a1e3e02052c244f8c2d3fcaf": {"t": 1785748617.988516, "n": 21764, "h": "5e386a4e39781197", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | ollama | claude-code | codex\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to local Ollama + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (OLLAMA_VLM_MODEL).\n# OLLAMA_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local Ollama provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "471791a8a6ce3634a32aaa253e1c70a4e57456fd": {"t": 1785748619.2354598, "n": 21409, "h": "88ddcdf9a7f44090", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | ollama | claude-code | codex\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to local Ollama + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (OLLAMA_VLM_MODEL).\n# OLLAMA_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local Ollama provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "2b373880460f829fc8eb4d5cc4cd24916a37d414": {"t": 1785748620.4877844, "n": 20107, "h": "bdefe41d3f9cf736", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | ollama | claude-code | codex\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to local Ollama + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local Ollama provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "3da69939787b84d57886f0534c93a30cca50a7d7": {"t": 1785748621.8911498, "n": 19993, "h": "36a1dd82c5400406", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | ollama | claude-code | codex\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to local Ollama + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local Ollama provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "5f942accfb5206612935302d783c545ab5852ae4": {"t": 1785748623.0948668, "n": 19498, "h": "27c54e4db57ac7bb", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | ollama | claude-code | codex\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to local Ollama + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local Ollama provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "f98f08c28a1f982cbeb618f386b88d07b68e066c": {"t": 1785748624.3112001, "n": 18708, "h": "78c9a3ef3a3b4387", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# options: anthropic | deepseek | gemini | ollama | claude-code\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to local Ollama + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local Ollama provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "f9f39e39ec0dbd646373d1155841ba3937bfef01": {"t": 1785748625.4337409, "n": 18267, "h": "f3bfef90191a4385", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# options: anthropic | deepseek | gemini | ollama | claude-code\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). Currently selects the non-coder starter memory set on first boot; UI simplification will follow. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on'.\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local Ollama provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "6285c4ec03e4bf9deefd5ad4d2704b8299bd912e": {"t": 1785748626.5637817, "n": 18049, "h": "8087ec182c3e8c2a", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# options: anthropic | deepseek | gemini | ollama | claude-code\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on'.\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local Ollama provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\nHEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "baca2b875b927d28552ddb9761a19b7f7e1a3eb2": {"t": 1785748627.81006, "n": 17645, "h": "ff0d328f73d7c4e3", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# options: anthropic | deepseek | gemini | ollama | claude-code\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on'.\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# Inject the current date & time into the system prompt each turn, so the agent knows 'now' and can tell when its training data is stale. Set to 'off' to omit it.\n# APERIO_INJECT_CLOCK=off\n\n# IANA timezone for the injected clock (e.g. 'America/New_York'). Empty uses the host system timezone.\n# APERIO_CLOCK_TZ=Europe/Sofia\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open (< IDLE_TIMEOUT).\nHEARTBEAT_INTERVAL_SECONDS=120\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "515a80d72611b3a41d6d96d37a181795664efe11": {"t": 1785748630.089395, "n": 17320, "h": "1f7182a27ccd113f", "c": "# ============================================================\n# Aperio — Configuration\n#\n# AUTO-GENERATED from lib/config.js — do not edit by hand.\n# Run `npm run gen:env` to regenerate. Edit the registry, not this file.\n#\n# This is the complete list of every variable Aperio reads, grouped by how\n# risky it is to touch:\n#\n# 1 START HERE safe — what most setups actually edit.\n# ── ✋ STOP ── past here is advanced; the defaults already work.\n# 2 OPTIONAL extra features (usually toggled in the app's UI instead).\n# 3 EXTERNAL keys/endpoints for third-party services.\n# 4 CORE/CRITICAL bootstrap + security plumbing; must live in .env.\n#\n# To configure by hand: copy this file to `.env` and edit the START HERE\n# block. Most people never go past the STOP line — run `npm start` and the\n# setup wizard in your browser writes a real `.env` for you.\n#\n# Precedence: .env > DB (Settings UI) > built-in default. Your `.env` wins by\n# default; set APERIO_CONFIG_PRECEDENCE=db (or flip it in the Settings UI) to\n# let UI-saved values override `.env` instead.\n#\n# NEVER commit .env to git.\n# ============================================================\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n# The few values most setups actually touch. Pick a provider and paste its key (or use local Ollama), then choose a model and port.\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# The few values most setups need to get started.\n\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • ollama → free, runs on your machine (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# options: anthropic | deepseek | gemini | ollama | claude-code\nAI_PROVIDER=ollama\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# LOCAL: if you chose ollama, set the model (install: ollama.ai).\n# Pick by RAM: <8GB → qwen2.5:3b · 8GB → gemma4:e4b · 24GB → gemma4:12b · 48GB+ → qwen3:30b-a3b\nOLLAMA_MODEL=qwen2.5:3b\n\n# Port the web app runs on.\nPORT=31337\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • env (default) → your .env wins, so terminal users configure everything from the file. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB.\n# • db → the Settings UI is authoritative; a saved value overrides .env, so you can manage everything from the panel instead of editing .env by hand.\n# Readable from .env (so it can force itself) but also editable in the Settings UI — flip it to 'db' there once you want to drive config from the panel.\n# options: db | env\n# APERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're here.\n# Aperio runs fine on the defaults below — most people never edit\n# past this line. Everything beyond is advanced: optional features\n# (normally toggled in the app's Settings panel) and the\n# bootstrap/security plumbing the app depends on.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n# Extra capabilities, all off or at their default until you turn them on. These are normally toggled in the app's Settings panel (saved to the database); they're listed here for headless / file-only setups. By default (APERIO_CONFIG_PRECEDENCE=env) what you set here wins; set it to db to let a value saved in the UI override what you set here instead.\n\n# ── File path safety ──────────────────────────────────────────\n# Seed-only: these populate the allowed-folders list on first run, then it's edited in the UI and persisted in the DB.\n\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Lets the model run a single allowlisted shell command inside the session workspace. NOT a sandbox — off by default.\n\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to local Ollama models.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Local Ollama models are lean chat models by default; list trusted ones to grant them tools + memory.\n\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Tools treated as destructive get strict handling: malformed JSON arguments are never auto-repaired (a 'fixed' string could silently corrupt a file or row) and a failed call returns a plain error instead of a coercion hint. The built-in list (write_file, edit_file, db_execute, run_shell, …) can't be weakened; you can only extend it.\n\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# ── Ollama (local) extras ─────────────────────────────────────\n# Defaults shown — override only if your Ollama setup differs.\n\n# Ollama chat API base URL.\n# OLLAMA_BASE_URL=http://localhost:11434\n\n# Model used for image understanding.\n# OLLAMA_VLM_MODEL=qwen2.5vl:3b\n\n# Context window (tokens) the app assumes for its trim/cap math. The real KV cache is sized by the server's OLLAMA_CONTEXT_LENGTH; Aperio passes this through when it starts Ollama for you. Bigger is NOT free — keep it within VRAM (16384–32768 suits a 7–12B model).\n# OLLAMA_NUM_CTX=32768\n\n# Server-side KV cache size. If you run `ollama serve` yourself, launch it with this matching OLLAMA_NUM_CTX.\n# OLLAMA_CONTEXT_LENGTH=32768\n\n# Used by the auto start/stop helper; prefer OLLAMA_BASE_URL. Set only if your Ollama host differs.\n# OLLAMA_HOST=http://localhost:11434\n\n# ── Gemini thinking budget ────────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# Boots a second verifier agent that reviews/revises answers until both agree or the round cap.\n\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# Regenerate stale wiki articles with a cheaper/local provider instead of the chat model.\n\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=ollama:llama3.1\n\n# Only honored for ollama: spawn `ollama serve` on first refresh if down.\nWIKI_REFRESH_AUTOSTART_OLLAMA=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (local, no key) or voyage (cloud). EMBEDDING_DIMS must match the model — changing it needs a fresh DB.\n\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Symbol/reference index of the workspace + document index so the model can navigate code and docs.\n\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on'.\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler and how long run history is kept.\n\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# Milliseconds. Raise if big local models or slow networks time out.\n\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# Ollama generate request timeout.\n# OLLAMA_FETCH_TIMEOUT_MS=300000\n\n# Ollama up/down health-check timeout.\n# OLLAMA_HEALTH_TIMEOUT_MS=3000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n# Credentials and connection details for third-party services. Leave blank to keep the integration off.\n\n# ── GitHub integration ────────────────────────────────────────\n# Token + webhook secret for the issue-triage tools. Both can also be set in the app (Settings → GitHub triage).\n\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Named connections the db_* tools can query (SQLite/Postgres/MySQL/SQL Server + the built-in 'aperio' store). Normally added in Settings → Database connections; the env var below is a headless seed.\n\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n# Read at startup before the database opens, so these MUST live in .env (the UI can't manage them). Wrong values can stop Aperio from starting or expose it on a network — change only with intent.\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# How and where Aperio stores its data. Read at startup before anything else, so these stay in .env.\n\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# Only used when DB_BACKEND=postgres (or Docker is detected).\n\nPOSTGRES_HOST=localhost\n\nPOSTGRES_PORT=5432\n\nPOSTGRES_DB=aperio\n\nPOSTGRES_USER=aperio\n\n# Change this!\nPOSTGRES_PASSWORD=aperio_secret\n\nDATABASE_URL=postgresql://aperio:aperio_secret@localhost:5432/aperio\n\n# ── Server ────────────────────────────────────────────────────\n\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Server + Ollama shut down this many seconds after the last tab closes.\nIDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open (< IDLE_TIMEOUT).\nHEARTBEAT_INTERVAL_SECONDS=120\n\n# Days of chat history to keep.\nSESSION_RETENTION_DAYS=14\n\n# ── Network security ──────────────────────────────────────────\n\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on local Ollama. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n"}, "2b4d143ca069ef7b115bf38e8e1cdfba32acc8de": {"t": 1786068048.7378194, "n": 25054, "h": "d54945eb5e2c0c4a", "c": "# Aperio — configuration bootstrap (AUTO-GENERATED)\n# Run `npm run gen:env` after editing lib/config.js; never edit this file by hand.\n# This is the complete list of every variable Aperio reads (122 total),\n# grouped by how risky it is to touch. Only the START HERE block is normally edited.\n# The same inventory, as readable markdown, is in docs/config-reference.md.\n# Copy to `.env`, edit the START HERE values, and run `npm start`.\n# .env wins when APERIO_CONFIG_PRECEDENCE=env (the active default below).\n# Use `npm run gen:env:check` to detect drift.\n\n# ════════════════════════════════════════════════════════════\n# 1 · START HERE — safe to edit\n# ════════════════════════════════════════════════════════════\n\n# ── ESSENTIALS ────────────────────────────────────────────────\n# Which AI backend to use.\n# • anthropic / deepseek / gemini → cloud; paste the matching API key below.\n# • llamacpp → free, runs on your machine via a vendored llama-server (no key).\n# • claude-code → uses your Claude Pro/Max subscription via the Agent SDK (requires the `claude` CLI logged in; do NOT also set ANTHROPIC_API_KEY).\n# • codex → uses the Codex CLI (`codex login` or CODEX_API_KEY for exec automation).\n# options: anthropic | deepseek | gemini | llamacpp | claude-code | codex\nAI_PROVIDER=llamacpp\n\n# Anthropic — console.anthropic.com\nANTHROPIC_API_KEY=\n\nANTHROPIC_MODEL=claude-haiku-4-5-20251001\n\n# DeepSeek — platform.deepseek.com\nDEEPSEEK_API_KEY=\n\nDEEPSEEK_MODEL=deepseek-v4-flash\n\n# Google Gemini — aistudio.google.com\nGEMINI_API_KEY=\n\nGEMINI_MODEL=gemini-2.0-flash\n\n# OpenAI key (only if you wire an OpenAI-compatible provider).\n# OPENAI_API_KEY=\n\n# Codex model when AI_PROVIDER=codex.\n# CODEX_MODEL=gpt-5.5\n\n# Optional API-key auth for `codex exec`; otherwise Codex uses cached CLI auth. Use Codex only in a trusted workspace: agent-run code can inherit process credentials.\n# CODEX_API_KEY=\n\n# Codex sandbox when AI_PROVIDER=codex. Prefer workspace-write unless the host is externally isolated.\n# options: read-only | workspace-write | danger-full-access\n# CODEX_SANDBOX=workspace-write\n\n# Codex approval policy for non-interactive provider runs. `never` avoids stalled UI prompts; pair it with a sandbox. Aperio's required MCP server is explicitly approved separately.\n# options: untrusted | on-request | never\n# CODEX_APPROVAL_POLICY=never\n\n# Tool-approval mode for Aperio's MCP server inside Codex runs. `approve` marks this explicitly configured, required server as trusted (needed because approval_policy=never cannot answer an MCP prompt).\n# options: auto | prompt | approve\n# CODEX_MCP_APPROVAL_MODE=approve\n\n# Seconds Codex waits for Aperio's MCP server to start before failing the run.\n# CODEX_MCP_STARTUP_TIMEOUT_SEC=20\n\n# Per-tool-call timeout (seconds) for Aperio's MCP server inside Codex runs.\n# CODEX_MCP_TOOL_TIMEOUT_SEC=300\n\n# Pass --ignore-rules to `codex exec` so repo rule files (AGENTS.md etc.) are skipped for provider runs.\n# CODEX_IGNORE_RULES=1\n\n# Codex reasoning-summary verbosity. `auto`/`concise`/`detailed` make the CLI emit `reasoning` items that Aperio renders as the collapsed thinking bubble (same UI as every other provider); `none` disables it. Doesn't add API cost — it's a summary of tokens already billed as reasoning_output_tokens.\n# options: auto | concise | detailed | none\n# CODEX_REASONING_SUMMARY=auto\n\n# Maximum distinct Codex tool/action items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_TOOL_CALLS=64\n\n# Maximum distinct Codex reasoning/tool/plan work items allowed in one turn. Live limit; set to 0 to disable it.\n# CODEX_TURN_MAX_STEPS=128\n\n# Maximum wall-clock milliseconds for one Codex turn. Live subprocess deadline; set to 0 to disable it.\n# CODEX_TURN_TIMEOUT_MS=1200000\n\n# Observed ceiling for aggregate processed input tokens in one Codex turn. The current CLI reports usage only at turn completion, so this preserves the answer and warns instead of live-aborting. Set to 0 to disable it.\n# CODEX_TURN_MAX_PROCESSED_TOKENS=300000\n\n# Alternative to running `claude` interactively when AI_PROVIDER=claude-code.\n# CLAUDE_CODE_OAUTH_TOKEN=\n\n# Working directory for the claude-code provider's CLI subprocess (AI_PROVIDER=claude-code). Left unset, the subprocess inherits the server process's own cwd — the repo root for a normal Aperio install, which is the intended behavior. Set only to sandbox that subprocess away from the real project tree (e.g. an isolated test harness).\n# APERIO_CLAUDE_CODE_CWD=\n\n# Port the web app runs on.\nPORT=31337\n\n# Lite profile for non-technical users (the desktop launchers set it). When on: the non-coder starter memory set is seeded on first boot, unset values default to the local llama.cpp engine + SQLite with the document index on, the web UI hides developer surfaces (code graph, DB browser, agents, skills, deep config) behind an Advanced toggle in Settings, and config precedence is always 'db' — the Settings UI rules; .env only spins the app up. Off unless set to exactly 'on'.\n# APERIO_LITE=on\n\n# Who wins when a setting is in BOTH the Settings UI (DB) and your .env:\n# • db (default) → the Settings UI is authoritative; a saved value overrides .env, so you manage everything from the app without editing files.\n# • env → the developer escape hatch: every line you write in .env wins, however few lines it holds. Settings that are ONLY in the DB (no .env entry) still apply — env mode overrides, it does not ignore the DB. This is also how to keep API keys file-only.\n# Readable from .env (so it can force itself) and editable in the Settings UI.\n# Ignored when APERIO_LITE=on: the lite profile is always 'db' (lite users configure everything in the UI; .env only spins the app up).\n# options: db | env\nAPERIO_CONFIG_PRECEDENCE=env\n\n# ╶───────────────────────────────────────────────────────────╴\n# ✋ STOP HERE unless you know why you're continuing.\n# Aperio runs fine with the defaults below. Everything beyond this line\n# is advanced: optional features, integrations, and security plumbing.\n# ╶───────────────────────────────────────────────────────────╴\n\n# ════════════════════════════════════════════════════════════\n# 2 · OPTIONAL FEATURES\n# ════════════════════════════════════════════════════════════\n\n# ── File path safety ──────────────────────────────────────────\n# Seed only. Comma-separated absolute paths that populate the allowed read-folders list on first run; edited in the UI thereafter. Project dir + session scratch are always allowed.\n# APERIO_ALLOWED_PATHS_TO_READ=/abs/path/one,/abs/path/two\n\n# Seed only. As above, for write access.\n# APERIO_ALLOWED_PATHS_TO_WRITE=/abs/path/one\n\n# ── Shell execution (run_shell tool) ──────────────────────────\n# Allow the run_shell tool (single allowlisted command in the session workspace). NOT a sandbox — grants host execution as your user. Only enable for trusted models/content.\n# APERIO_ENABLE_SHELL=1\n\n# By default only cloud providers get run_shell. Set to also offer it to the local llama.cpp model.\n# APERIO_SHELL_LOCAL=1\n\n# Max bytes of shell output captured before truncation.\n# APERIO_SHELL_MAX_OUTPUT_BYTES=48000\n\n# ── Capable local models ──────────────────────────────────────\n# Comma-separated local model names you trust to get the full tools + memory flow (others stay lean chat models).\n# APERIO_CAPABLE_MODELS=qwen3:32b,llama3.1:70b\n\n# Comma-separated local model names that still need forced auto-recall (a behavior override, not just the recall pointer). Falls back to APERIO_CAPABLE_MODELS when unset. Remove a model from this list once it reliably calls recall on its own, without losing tools/memory from APERIO_CAPABLE_MODELS.\n# APERIO_RECALL_SCAFFOLD_MODELS=qwen3:32b,llama3.1:70b\n\n# Context windows at or below this token count are treated as small: the per-turn tool set is capped so re-sent tool schemas don't crowd out the result and reasoning the model needs. Larger windows are never touched.\n# APERIO_SMALL_WINDOW_TOKENS=8192\n\n# Maximum tools attached per turn on a small-window model (see APERIO_SMALL_WINDOW_TOKENS). The recall floor and the turn's intent tools are kept first.\n# APERIO_SMALL_WINDOW_MAX_TOOLS=10\n\n# ── Tool-call safety ──────────────────────────────────────────\n# Comma-separated EXTRA tool names to treat as destructive, added to the built-in set. Use for your own / MCP tools that mutate state. Built-ins can't be removed — this only adds stricter handling.\n# APERIO_EXTRA_DESTRUCTIVE_TOOLS=my_custom_writer,my_db_mutator\n\n# Store a complete tool result outside model context when it exceeds this token count. Aperio lowers the effective threshold to 25% of the active model context window. 0 disables the token threshold; the byte threshold still applies.\n# APERIO_TOOL_RESULT_OFFLOAD_TOKENS=20000\n\n# Store a complete tool result outside model context when its redacted UTF-8 content exceeds this byte count. 0 disables the byte threshold; the token threshold still applies. Stored session artifacts follow SESSION_RETENTION_DAYS; background-run artifacts follow AGENT_RUN_RETENTION_DAYS.\n# APERIO_TOOL_RESULT_OFFLOAD_BYTES=80000\n\n# ── llama.cpp (local) extras ──────────────────────────────────\n# Port the vendored llama-server listens on (127.0.0.1 only).\n# LLAMACPP_PORT=8080\n\n# Private directory containing Aperio's pinned llama-server binary. Docker points this into its persistent runtime volume; normal installs keep it inside the app folder.\n# LLAMACPP_VENDOR_DIR=vendor/llamacpp\n\n# llama-server chat API base URL.\n# LLAMACPP_BASE_URL=http://127.0.0.1:8080\n\n# Hugging Face repo[:quant] for the main model (llama-server -hf format). Becomes the hf-repo of the router's `aperio-main` preset entry; requests send that stable alias as the `model` field (not the raw repo id, which would load a second, full-context copy).\n# LLAMACPP_MODEL=unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with up to 8 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_8=unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 8 GB and up to 16 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_16=unsloth/Qwen3.5-9B-GGUF:Q4_K_M\n\n# Recommended llama.cpp model for machines with more than 16 GB and up to 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_24=unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL\n\n# Recommended llama.cpp model for machines with more than 24 GB RAM (Hugging Face repo[:quant]).\n# LLAMACPP_MODEL_TIER_32=unsloth/Qwen3.6-35B-A3B-MTP-GGUF:UD-Q4_K_XL\n\n# Hugging Face repo[:quant] used for image understanding; llama-server's router loads/swaps it on demand.\n# LLAMACPP_VLM_MODEL=ggml-org/Qwen2.5-VL-7B-Instruct-GGUF\n\n# JSON object keyed by HF repo path (with optional :quant suffix) overriding both cached GGUF inspection and the curated model_facts database catalog. Each value has { sizeGB, maxContext, kvBytesPerToken, architecture?, activeParams? }. Resolution order: configured override → cached GGUF → model_facts catalog → conservative generic facts.\n# APERIO_MODEL_FACTS_OVERRIDES=\n\n# Context window (tokens) the app assumes for its trim/cap math. Successor of OLLAMA_NUM_CTX. The real KV cache is sized by LLAMACPP_SERVE_CTX (the model's --ctx-size in the router preset); Aperio passes this through when it starts llama-server for you.\n# LLAMACPP_CTX=32768\n\n# Minimum served context for Aperio's default Gemma 4 E2B local agent on machines with at least 7.5 GiB RAM. Prevents the identity, memory pointer, and minimum tool schema from exceeding a tiny llama.cpp window.\n# LLAMACPP_MIN_AGENT_CTX=8192\n\n# Successor of OLLAMA_CONTEXT_LENGTH. Server-side KV cache size (--ctx-size) for the main model in the router preset.\n# LLAMACPP_SERVE_CTX=32768\n\n# Override where llama-server caches downloaded GGUF weights. Leave unset to use the standard Hugging Face hub cache (HF_HUB_CACHE, else $HF_HOME/hub, else ~/.cache/huggingface/hub) — the same location llama-cli and every other HF tool use, so models you already have are reused instead of re-downloaded into the app folder. Set a path only to force a non-standard location.\n# LLAMA_CACHE=~/.cache/huggingface/hub\n\n# off (default): when every model in the router preset is already cached, llama-server starts with --offline, so it never re-checks Hugging Face on load — an upstream re-upload of the same repo can't trigger a surprise multi-GB re-download mid-conversation. on: revalidate against Hugging Face on every model load and pull upstream updates. Models not yet cached are always downloaded regardless of this setting.\n# options: off | on\n# LLAMACPP_CHECK_UPDATES=off\n\n# Hardware/perf preset for the local llama.cpp engine. balanced (default) = current sizing, unchanged. fast-low-vram = lower context ceiling, quantized KV cache (-ctk/-ctv q8_0), flash attention, a MoE-preferred model pick, MoE expert layers offloaded to CPU (--n-cpu-moe), and only one resident model at a time (--models-max 1) — the biggest speed win on tight VRAM. long-context = raised context ceiling and a larger share of RAM committed to the KV cache; trades throughput for a bigger window — expect noticeably slower tokens/sec. quality = the biggest model your RAM allows, accepting slower tokens/sec.\n# options: balanced | fast-low-vram | long-context | quality\n# APERIO_LOCAL_PERF_PROFILE=balanced\n\n# ── Extended thinking budgets ─────────────────────────────────\n# Tokens reserved for reasoning. 0 = off. Range when on: 512–24576.\n# GEMINI_THINKING_BUDGET=0\n\n# Tokens reserved for Claude's extended thinking. 0 = off (default — thinking tokens are billed output). Range when on: 1024–24576. Reasoning streams as a collapsed bubble like every other provider.\n# ANTHROPIC_THINKING_BUDGET=0\n\n# ── Round-table mode (two-agent cross-review) ─────────────────\n# <provider>:<model>,<provider>:<model> — first = answerer, second = reviewer.\n# ROUNDTABLE_AGENTS=anthropic:claude-haiku-4-5-20251001,deepseek:deepseek-chat\n\n# Domain characters layered on each agent's role. \"charA,charB\" — first → answerer, second → reviewer.\n# E.g. security audit: software-architect,security-engineer.\n# ROUNDTABLE_CHARACTERS=software-architect,code-reviewer\n\n# Max review/revise rounds before giving up on agreement.\n# ROUNDTABLE_MAX_ROUNDS=3\n\n# ── Wiki refresh provider ─────────────────────────────────────\n# <provider>:<model> to regenerate stale wiki articles with instead of the chat model. Unset disables (serves the stale body).\n# WIKI_REFRESH_PROVIDER=llamacpp:Qwen/Qwen2.5-3B-Instruct-GGUF:Q4_K_M\n\n# Only honored for llamacpp: start the vendored llama-server on first refresh if down.\n# WIKI_REFRESH_AUTOSTART_LLAMACPP=true\n\n# ── Embeddings ────────────────────────────────────────────────\n# transformers (default, fully local, no key) | voyage (cloud). transformers downloads mxbai-embed-large-v1 on first run.\n# options: transformers | voyage\n# EMBEDDING_PROVIDER=transformers\n\n# Vector size. Must match your embedding model; changing it requires a fresh database.\n# EMBEDDING_DIMS=1024\n\n# Voyage key — dash.voyageai.com (50M tokens/month free). Only needed when EMBEDDING_PROVIDER=voyage.\n# VOYAGE_API_KEY=pa-xxxxx-xxxx\n\n# Voyage embedding model (used when EMBEDDING_PROVIDER=voyage).\n# VOYAGE_MODEL=voyage-3\n\n# Local model cache directory for the transformers embedder.\n# TRANSFORMERS_CACHE=~/.cache/aperio/transformers\n\n# Semantic skill-match rescue: when keyword matching finds no skill for a turn, fall back to embedding similarity so paraphrases still attach the right skill. Fills blanks only — never overrides a keyword match. Requires an embedder (EMBEDDING_PROVIDER). on to enable.\n# APERIO_SKILL_SEMANTIC=on\n\n# Cosine floor for the semantic skill rescue. Empty = per-provider default (transformers 0.54; voyage not yet calibrated — run skills/autotune/calibrate.mjs with your key and set this). Higher = fewer, more confident matches.\n# APERIO_SKILL_SEMANTIC_FLOOR=\n\n# ── Code & document graph ─────────────────────────────────────\n# Build a symbol/reference index of the workspace. Off unless set to exactly 'on'.\n# APERIO_CODEGRAPH=on\n\n# Build a document index (MD/TXT/HTML/PDF/DOCX/XLSX/PPTX/EML). Off unless set to exactly 'on' (the lite profile turns it on by default).\n# APERIO_DOCGRAPH=on\n\n# Target chunk size (tokens) for document indexing.\n# DOCGRAPH_CHUNK_TOKENS=512\n\n# Token overlap between adjacent document chunks.\n# DOCGRAPH_CHUNK_OVERLAP=64\n\n# Max rows read per spreadsheet during document indexing.\n# DOCGRAPH_XLSX_MAX_ROWS=2000\n\n# Extra regex patterns (comma-separated) for cross-reference extraction, e.g. ticket IDs.\n# DOCGRAPH_REF_PATTERNS=\\bACME-\\d+\\b\n\n# Auto-promote high-confidence docgraph facts (amount_due/grand_total + due_date/invoice_date/service_period) into the memory store for trend-question recall. Off unless set to exactly 'on'.\n# DOCGRAPH_AUTO_MEMORY=on\n\n# ── Background agents (scheduled, chat-less jobs) ─────────────\n# Master switch for the background-agent scheduler. 'on' runs enabled jobs in var/agents/jobs.json on their interval. Off by default — nothing fires. See docs/background-agents.md.\n# APERIO_AGENT_JOBS=on\n\n# Days of background-agent run history to keep (GC'd daily). Unset or 0 keeps it forever; runs are also deletable in the UI.\n# AGENT_RUN_RETENTION_DAYS=30\n\n# ── Agent planning loop (experimental) ────────────────────────\n# Ask the model to lead multi-step turns with a JSON plan, then track drift between the plan and what actually ran (plan_created/plan_step/plan_drift events). Fail-safe: no plan, or an invalid one, and the turn proceeds exactly as it does with this off. Off unless set to exactly 'on'.\n# APERIO_AGENT_PLANNING=on\n\n# ── Privacy ───────────────────────────────────────────────────\n# How to handle tier-2 (sensitive) memories on cloud providers. 'withhold' (default) filters them out of recall results. 'redact' sends them with PII scrubbed. Tier-3 (private) is always withheld; tier-1 (normal) is always shared.\n# options: withhold | redact\n# APERIO_CLOUD_SENSITIVE_MODE=withhold\n\n# ── Request timeouts (advanced) ───────────────────────────────\n# DeepSeek API request timeout.\n# DEEPSEEK_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp generate request timeout.\n# LLAMACPP_FETCH_TIMEOUT_MS=300000\n\n# llama.cpp up/down health-check timeout.\n# LLAMACPP_HEALTH_TIMEOUT_MS=3000\n\n# Timeout for the local VLM image-analysis bridge (LLAMACPP_VLM_MODEL).\n# LLAMACPP_VLM_TIMEOUT_MS=300000\n\n# Timeout for one-shot `codex exec` completions (read-only helper calls, not the chat loop).\n# CODEX_COMPLETION_TIMEOUT_MS=120000\n\n# ── Logging & diagnostics ─────────────────────────────────────\n# How long to keep rotated log files (e.g. 7d, 30d, 14d).\n# APERIO_LOG_RETENTION=30d\n\n# Days to keep per-session llama-server debug logs (var/llamacpp/<session-id>.log). Pruned daily; minimum 1.\n# LLAMACPP_LOG_RETENTION_DAYS=1\n\n# Surface subprocess stderr that is hidden by default.\n# DEBUG=1\n\n# Suppress the shared llama-server log file. Session-scoped logs are still controlled by the retention setting.\n# APERIO_NO_LLAMA_LOG=1\n\n# ════════════════════════════════════════════════════════════\n# 3 · EXTERNAL INTEGRATIONS — keys & endpoints\n# ════════════════════════════════════════════════════════════\n\n# ── GitHub integration ────────────────────────────────────────\n# Personal access token for the github tools. Read needs no token for public repos; write (create/update issue) requires a `repo`-scoped token. github.com/settings/tokens\n# GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx\n\n# Shared secret for the issue-triage webhook (POST /api/github/webhook). When set, deliveries are HMAC-verified; unset, the route refuses all requests (503). The daily triage job also polls, so the webhook is optional.\n# GITHUB_WEBHOOK_SECRET=\n\n# ── Database tool (SQL connections) ───────────────────────────\n# Advanced/headless seed: a JSON array of database connections for the db_* tools. Normally you add these in Settings → Database connections (passwords are encrypted at rest there). Connections saved in the UI take precedence over names listed here. The built-in 'aperio' connection (your own store, read-only) always exists and is not listed here.\n# DB_CONNECTIONS=[{\"name\":\"shop\",\"engine\":\"postgres\",\"host\":\"localhost\",\"port\":5432,\"database\":\"shop\",\"user\":\"ro\",\"password\":\"secret\",\"readOnly\":true}]\n\n# ════════════════════════════════════════════════════════════\n# 4 · CORE / CRITICAL — bootstrap & security\n# ════════════════════════════════════════════════════════════\n\n# ── Database (bootstrap) ──────────────────────────────────────\n# 'postgres' (Docker required) | 'sqlite' (zero-config, default).\n# Default: auto-detect — Postgres if Docker is running, else SQLite.\n# options: sqlite | postgres\n# DB_BACKEND=sqlite\n\n# Where SQLite stores data (default shown).\n# SQLITE_PATH=./.sqlite/aperio.db\n\n# Encrypt the SQLite database file on disk with AES-256-GCM. Key lives in your OS keychain. Off by default; SQLite only.\n# APERIO_DB_ENCRYPT=1\n\n# ── Postgres ──────────────────────────────────────────────────\n# POSTGRES_HOST=localhost\n\n# POSTGRES_PORT=5432\n\n# POSTGRES_DB=aperio\n\n# POSTGRES_USER=aperio\n\n# Change this!\n# POSTGRES_PASSWORD=aperio_secret\n\n# DATABASE_URL=postgresql://aperio:aperio_secret@localhost:8008/aperio\n\n# ── Server ────────────────────────────────────────────────────\n# Default 127.0.0.1 (loopback only — safe). Set 0.0.0.0 for LAN access only if you understand the risks.\n# HOST=0.0.0.0\n\n# Idle auto-shutdown after the last browser tab closes. 'auto' = on only for the local llama.cpp provider; 'on' = always (the lite desktop launchers set this so a windowless/hidden server still self-stops on any provider); 'off' = never (for always-on server deployments).\n# options: auto | on | off\n# IDLE_SHUTDOWN=auto\n\n# Server + llama.cpp engine shut down this many seconds after the last tab closes.\n# IDLE_TIMEOUT_SECONDS=180\n\n# How often the browser pings to say a tab is still open. Keep it well under IDLE_TIMEOUT (≤ 1/3) so a throttled or dropped ping doesn't cause a false shutdown.\n# HEARTBEAT_INTERVAL_SECONDS=60\n\n# Days of chat history to keep.\n# SESSION_RETENTION_DAYS=14\n\n# Server-side fallback locale when no cookie or Accept-Language match is found. Must match a locale key in public/scripts/i18n.js LOCALE_META.\n# DEFAULT_LOCALE=en\n\n# Language for the terminal UI. A saved CLI preference wins over this value.\n# APERIO_UI_LANG=en\n\n# Optional path for persisted terminal preferences such as the examples toggle.\n# APERIO_CLI_PREFS=./var/cli-prefs.json\n\n# ── Network security ──────────────────────────────────────────\n# Extra hostnames accepted by the Host-header / DNS-rebinding guard (comma-separated). Add these if you reach Aperio via a reverse proxy or LAN name.\n# APERIO_ALLOWED_HOSTS=aperio.lan,192.168.1.50\n\n# Content-Security-Policy mode. 'on' enforces the browser policy (default), 'report' sends it as report-only during rollout, and 'off' disables CSP temporarily for troubleshooting.\n# options: on | report | off\n# APERIO_CSP=on\n\n# Opt-in shared-secret auth. When set, every /api request and WebSocket must present it (Authorization: Bearer, X-Aperio-Token, or ?token=). Set a long random value before exposing Aperio on a network.\n# APERIO_AUTH_TOKEN=\n\n# By default the memory inference/dedup workers run only on the local llama.cpp provider. Set to 1 to let them run on a cloud provider too (your memories get sent to it).\n# APERIO_CLOUD_MEMORY_WORKERS=1\n\n# Allow the fetch_url tool to reach private/loopback addresses (SSRF guard off). Leave unset unless you specifically need it.\n# APERIO_ALLOW_INTERNAL_FETCH=1\n\n# Restrict outbound fetches to these hosts (comma-separated). Unset = no allowlist restriction.\n# APERIO_EGRESS_ALLOWLIST=api.github.com,example.com\n\n# Advanced: force the active provider to be treated as local (affects tool/memory gating).\n# APERIO_PROVIDER_LOCAL=1\n\n# Advanced: bypass the startup refusal to run Postgres with the shipped default password. Do NOT set in production.\n# APERIO_ALLOW_DEFAULT_DB_PASSWORD=1\n\n# ── Hosting hardening ─────────────────────────────────────────\n# Opt-in TLS. Set BOTH cert + key (PEM) to serve HTTPS; leave both unset for plain HTTP on loopback. Aperio does not generate certs.\n# APERIO_TLS_CERT=/path/to/cert.pem\n\n# TLS private key (PEM). See APERIO_TLS_CERT.\n# APERIO_TLS_KEY=/path/to/key.pem\n\n# Opt-in at-rest session encryption (AES-256-GCM). When set, session transcripts are encrypted on disk; existing plaintext still loads.\n# APERIO_SESSION_KEY=\n\n# Which browser to open the UI in on startup (a private/incognito window). Falls back to the OS default if not installed. Use 'default' to skip the private window.\n# options: firefox | firefox-dev | librewolf | mullvad | chrome | chromium | brave | edge | tor | ddg | default\n# APERIO_BROWSER=firefox\n\n# Launch the chosen browser with a dedicated profile under var/browser-profiles/<browser>, isolating Aperio's cookies/storage/extensions. Ignored for tor/ddg.\n# APERIO_BROWSER_ISOLATED=1\n\n# Override the directory containing terminal locale JSON files. Intended for relocations and custom bundles.\n# APERIO_LOCALES_DIR=./public/locales\n"}}}}