mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
#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:
co-authored by
Claude Opus 4.8
parent
44b5935507
commit
a727ade87b
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user