fix: forward gemini keys in wrapper (#698), skip _pending sidecars in OKF scan (#695)

#698: the Docker wrapper's -e forwarding allowlist carried every provider
credential except GEMINI_API_KEY / GOOGLE_API_KEY, so a gemini provider or
embedder selected inside the container never saw its key and failed with
"provider not configured". Add both to the allowlist; guard with a packaging
test naming every forwarded provider key.

#695: the OKF conformance scan that feeds the pre-migration backup gate
flagged auto-improve `_pending/` staging sidecars (no frontmatter, never
migrated) as nonconformant, so once the backup receipt's archive was deleted
every boot re-archived the whole data dir. Skip the `_pending/` subtree,
matching the watcher indexer and the #669 ledger skip.

Closes #698
Closes #695

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MDbhmszrjG9s5MrPrTuNtm
This commit is contained in:
AkitaOnRails
2026-09-10 12:50:44 -03:00
co-authored by Claude Opus 4.8
parent 44b5935507
commit a727ade87b
4 changed files with 123 additions and 0 deletions
+15
View File
@@ -17,6 +17,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
now uses the import instead of a prose pointer (#680).
### Fixed
- The Docker wrapper (`bin/ai-memory`) now forwards `GEMINI_API_KEY` and
`GOOGLE_API_KEY` into the container. Every other provider credential was on
the `-e` forwarding allowlist, but these two were missing, so
`AI_MEMORY_LLM_PROVIDER=gemini` (or the gemini embedder) reached the server
while its key did not — the process then failed with `provider not
configured: GEMINI_API_KEY or GOOGLE_API_KEY` even though the operator had
exported it (#698).
- `serve` no longer re-archives the whole data directory on every boot once the
pre-migration backup receipt's archive has been deleted and auto-improve
`_pending/` sidecars exist. The OKF conformance scan that feeds the backup
gate flagged those staging sidecars (which carry no frontmatter and are never
migrated — SQLite owns their approval state) as nonconformant, so it kept
falling through to a full archive. The scan now skips the `_pending/` subtree,
matching the watcher indexer and the existing ledger skip (#695, same class as
#669).
- A bare `LLM_BASE_URL` in the environment no longer redirects providers that
talk to a fixed vendor endpoint. The variable is a cross-tool convention an
operator exports once for a local Ollama, and ai-memory fed it to every
+2
View File
@@ -596,6 +596,8 @@ for var in \
ANTHROPIC_OAUTH_TOKEN \
CLAUDE_CODE_OAUTH_TOKEN \
OPENAI_API_KEY \
GEMINI_API_KEY \
GOOGLE_API_KEY \
COPILOT_GITHUB_TOKEN \
GITHUB_COPILOT_API_TOKEN \
COPILOT_API_URL \
@@ -488,6 +488,32 @@ fn posix_wrapper_auto_selects_podman_when_docker_is_unavailable() {
);
}
#[test]
fn wrapper_forwards_every_supported_provider_api_key() {
// The wrapper runs the server in a container, so any provider credential
// the operator exports must be on the `-e` forwarding allowlist or it
// never reaches the process and the provider reports "not configured".
// Gemini/Google were missing while every other provider key was
// forwarded (#698), so the guard names each key the config layer reads.
let wrapper = read_repo("bin/ai-memory");
for key in [
"ANTHROPIC_API_KEY",
"ANTHROPIC_OAUTH_TOKEN",
"OPENAI_API_KEY",
"GEMINI_API_KEY",
"GOOGLE_API_KEY",
"VOYAGE_API_KEY",
"COPILOT_GITHUB_TOKEN",
"LLM_API_KEY",
"EMBEDDING_API_KEY",
] {
assert!(
wrapper.contains(&format!(" {key} \\")),
"wrapper must forward {key} into the container"
);
}
}
#[test]
fn wrapper_updates_and_install_docs_use_verified_release_assets() {
let wrapper = read_repo("bin/ai-memory");
@@ -205,6 +205,18 @@ fn nonconformant_files(wiki_root: &Path) -> WikiResult<Vec<PathBuf>> {
if path.file_name().is_some_and(|n| n == ".git") {
continue;
}
// Staging sidecars under `_pending/` (auto-improve proposals)
// are not pages: SQLite owns their approval state, and
// `render_auto_improve_sidecar` writes them with no OKF
// frontmatter. Left in scope they read as nonconformant on
// every boot, and since this scan feeds the pre-migration
// backup gate, that re-archives the whole data dir each time
// once the receipt's archive is gone (#695, same class as the
// ledger skip for #669). Skip the subtree, matching the
// watcher indexer's own `_pending/` exclusion.
if path.file_name().is_some_and(|n| n == "_pending") {
continue;
}
stack.push(path);
} else if ft.is_file()
&& path.extension().is_some_and(|e| e == "md")
@@ -817,6 +829,74 @@ mod tests {
);
}
/// A conformant store whose only "nonconformant" files are auto-improve
/// staging sidecars under `_pending/` must not re-archive the data dir.
/// The sidecars carry no OKF frontmatter and are never migrated (SQLite
/// owns approval state), so leaving them in scope made every boot fall
/// through the backup gate once the receipt's archive was gone (#695).
#[tokio::test]
async fn a_conformant_store_with_pending_sidecars_skips_the_backup() {
let tmp = TempDir::new().unwrap();
let store = Store::open(tmp.path()).unwrap();
let ws = store.writer.get_or_create_workspace("w").await.unwrap();
let proj = store
.writer
.get_or_create_project(ws, "p", None)
.await
.unwrap();
let wiki = Wiki::new(tmp.path(), store.writer.clone()).unwrap();
wiki.write_page(WritePageRequest {
workspace_id: ws,
project_id: proj,
path: PagePath::new("notes/setup.md").unwrap(),
frontmatter: serde_json::json!({"title": "setup"}),
body: "how it was set up".into(),
tier: Tier::Semantic,
pinned: false,
title: None,
admission_ctx: None,
author_id: None,
actor: ai_memory_core::ActorContext::anonymous(),
})
.await
.unwrap();
let pending_dir = tmp
.path()
.join("wiki")
.join(ws.to_string())
.join(proj.to_string())
.join("_pending")
.join("auto-improve");
std::fs::create_dir_all(&pending_dir).unwrap();
// Exactly what `render_auto_improve_sidecar` writes: a heading, no
// frontmatter at all.
std::fs::write(
pending_dir.join("0001.md"),
"# Pending auto-improvement proposal\n\nProposal body.\n",
)
.unwrap();
let wiki_root = tmp.path().join("wiki");
let pending = nonconformant_files(&wiki_root).unwrap();
assert!(
pending.is_empty(),
"a _pending/ sidecar must not be listed as nonconformant: {pending:?}"
);
let dest = TempDir::new().unwrap();
let receipt = snapshot_before_db_migration(tmp.path(), Some(dest.path())).unwrap();
assert!(
receipt.is_none(),
"a conformant store plus a _pending/ sidecar must not trigger a backup"
);
assert_eq!(
std::fs::read_dir(dest.path()).unwrap().count(),
0,
"the _pending/ sidecar caused a full data-dir archive"
);
}
/// Control: an ordinary page still missing its `type` frontmatter must
/// still be flagged, so the ledger exclusion is not swallowing real 1.x
/// pages.