mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(memory): use stable document_id as sync upsert key (fixes #4947 Bug 2 secret-guard sync failure) (#4953)
This commit is contained in:
@@ -254,9 +254,33 @@ impl MemoryClient {
|
||||
document_id: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
let namespace = format!("skill-{}", skill_id.trim());
|
||||
// The upsert dedup key must be a stable, opaque identifier — never
|
||||
// free-form human text. Sync providers pass a `{toolkit}:{id}`
|
||||
// `document_id` (e.g. `gmail:<message_id>`); use it as the key so the
|
||||
// provider's human-readable title (an email subject can legitimately
|
||||
// contain verification codes, token-rotation notices, or other
|
||||
// secret-/PII-looking strings) is never run through the
|
||||
// `upsert_document` secret/PII namespace/key guard. A single such
|
||||
// subject would otherwise abort the whole non-tolerant provider sync
|
||||
// with `document namespace/key cannot contain secrets` (see #4947).
|
||||
// Callers without a stable id (e.g. LinkedIn enrichment) keep the
|
||||
// title as the key, unchanged.
|
||||
let stable_id = document_id
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|id| !id.is_empty());
|
||||
let key = match stable_id {
|
||||
Some(id) => id.to_string(),
|
||||
None => title.to_string(),
|
||||
};
|
||||
tracing::debug!(
|
||||
namespace = %namespace,
|
||||
key_from_document_id = stable_id.is_some(),
|
||||
"[memory_store] store_skill_sync: upserting synchronized document"
|
||||
);
|
||||
let input = NamespaceDocumentInput {
|
||||
namespace,
|
||||
key: title.to_string(),
|
||||
key,
|
||||
title: title.to_string(),
|
||||
content: content.to_string(),
|
||||
source_type: source_type.unwrap_or_else(|| "doc".to_string()),
|
||||
|
||||
@@ -109,6 +109,77 @@ async fn clear_namespace_removes_all_docs_in_namespace() {
|
||||
assert!(docs_arr.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn store_skill_sync_with_secret_like_title_uses_stable_document_id_as_key() {
|
||||
// Regression for #4947 (Bug 2): a Composio provider sync passes the
|
||||
// provider's human-readable title as the document title, but the upsert
|
||||
// *key* must be the stable opaque `document_id` (`gmail:<message_id>`),
|
||||
// NOT the title. Some email subjects legitimately look secret-like
|
||||
// (verification codes, token-rotation notices), and `upsert_document`
|
||||
// rejects any secret-like namespace/key. Because gmail's tinycortex
|
||||
// pipeline does not tolerate scope errors, one such subject would abort
|
||||
// the entire scheduled sync with `document namespace/key cannot contain
|
||||
// secrets`, leaving the source stale ("Last synced 17d ago").
|
||||
let (_tmp, client) = make_client();
|
||||
|
||||
// A subject that trips the secret guard. Assert the precondition so the
|
||||
// test cannot silently pass if the detector's patterns change.
|
||||
let secret_like_title = "Security alert: token glpat-aaaaaaaaaaaaaaaaaaaa was created";
|
||||
assert!(
|
||||
crate::openhuman::memory_store::safety::has_likely_secret(secret_like_title),
|
||||
"test title must trip the secret detector for this regression to be meaningful"
|
||||
);
|
||||
|
||||
// With a stable document_id provided, the write must succeed — the key is
|
||||
// the opaque id, not the secret-like subject. Before the fix the key was
|
||||
// the title and this returned the secrets error.
|
||||
client
|
||||
.store_skill_sync(
|
||||
"gmail",
|
||||
"conn-1",
|
||||
secret_like_title,
|
||||
"email body",
|
||||
Some("composio-provider-incremental".into()),
|
||||
None,
|
||||
Some("medium".into()),
|
||||
None,
|
||||
None,
|
||||
Some("gmail:19af23bc00112233".into()),
|
||||
)
|
||||
.await
|
||||
.expect("secret-like subject must not block a stable-id-keyed sync write");
|
||||
|
||||
// The document is persisted and keyed by the stable id, so a second sync
|
||||
// of the same message dedups (updates in place) rather than duplicating.
|
||||
client
|
||||
.store_skill_sync(
|
||||
"gmail",
|
||||
"conn-1",
|
||||
secret_like_title,
|
||||
"email body v2",
|
||||
Some("composio-provider-incremental".into()),
|
||||
None,
|
||||
Some("medium".into()),
|
||||
None,
|
||||
None,
|
||||
Some("gmail:19af23bc00112233".into()),
|
||||
)
|
||||
.await
|
||||
.expect("re-sync of same message id must succeed");
|
||||
|
||||
let docs = client.list_documents(Some("skill-gmail")).await.unwrap();
|
||||
let arr = docs
|
||||
.get("documents")
|
||||
.and_then(|v| v.as_array())
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
assert_eq!(
|
||||
arr.len(),
|
||||
1,
|
||||
"stable-id key must dedupe both syncs into a single document"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn clear_skill_memory_targets_prefixed_namespace() {
|
||||
let (_tmp, client) = make_client();
|
||||
|
||||
Reference in New Issue
Block a user