The embedding cache was keyed by skill_id alone, so any edit to a
SKILL.md body or description produced stale embeddings that
get_or_compute_embedding kept serving until a manual invalidate_cache
call or a file deletion. Previously this was mostly invisible because
select_skills_with_llm was the only caller exercising the cache; after
the preceding commit wires search_skills through the same path the
staleness becomes observable on every MCP query.
Use "{skill_id}:{sha256(embedding_text)[:16]}" as the cache key, so
any change to the text produced by _build_embedding_text (name +
description + body, truncated to SKILL_EMBEDDING_MAX_CHARS) causes
an automatic cache miss and a fresh embedding. Both
get_or_compute_embedding and _embedding_rank are updated.
Bounded growth: on each successful new compute, older entries with
the same "{skill_id}:" prefix are pruned in the same write. Net
result: at most one cached embedding per skill_id at any time, aside
from transient migration state.
Backward compatibility: existing pickle files keyed by skill_id alone
are migrated in place on first lookup (no API call needed); the old
key is dropped after migration.
invalidate_cache(skill_id) now removes every content-addressed entry
and any legacy entry for that skill_id, so historical versions do
not leak across evolutions.
Functional benchmark on a 28-skill local registry with
text-embedding-3-small via OpenRouter: top-1 match identity preserved
on all test queries, score drift below 0.001, warm latency
~260-400ms/query (unchanged from the previous commit).
Both paths in search_skills/hybrid_search_skills now go through a
shared SkillRanker singleton:
- SkillSearchEngine._bm25_phase: previously instantiated a fresh
SkillRanker per call, reloading the pickle cache each time.
- hybrid_search_skills candidate loop: previously generated
embeddings via generate_embedding on every query, ignoring the
persistent cache entirely.
The persistent pickle at
.openspace/skill_embedding_cache/skill_embeddings_v1.pkl is
reused across invocations and survives process restarts.
Candidates without a stable skill_id are skipped to avoid cache
key collisions.
On a 28-skill local registry with text-embedding-3-small via
OpenRouter, query latency drops from 8-14s to ~300ms after
warm-up. Top-1 match identity is preserved on all test queries
(score drift <0.001).
Cloud candidates that already carry _embedding from the
server-side search endpoint are skipped and unchanged.
The original change removed atomacos from dependencies but left it
bundled in the same try/except as AppKit. Without atomacos installed,
the ImportError made MACOS_LIBS_AVAILABLE=False, silently disabling
every macOS feature (screenshots, window control, etc.) even though
they never use atomacos.
- Split import into independent try/except blocks
- Guard get_accessibility_tree() with ATOMACOS_AVAILABLE
- Condense redundant comments in requirements files
- Tighten README note into a blockquote
Error handlers in execute_task, fix_skill, and upload_skill returned
traceback.format_exc() to MCP clients, exposing internal file paths,
code structure, and potentially sensitive details. The full traceback
is already logged server-side via logger.error(exc_info=True).
Remove the traceback field from client-facing error responses and
clean up the unused traceback import.
Closes#19
Made-with: Cursor
When the LLM returns a short tool name that doesn't match the deduped
key in tool_map, the fallback scan correctly resolves tool_obj via
schema.name. However the execution branch still checked
`tool_name not in tool_map` and passed `tool_map[tool_name]`, so
fallback-resolved tools were never executed.
Change the condition to check `tool_obj is None` and pass `tool_obj`
directly to _execute_tool_call.
Closes#15
Made-with: Cursor
__main__.py calls Logger.set_level(args.log_level) when --log-level
is passed, but the method did not exist on Logger, causing an
AttributeError. Add set_level(level: str) that resolves the name
to a logging constant and reconfigures via configure(force=True).
Closes#13
Made-with: Cursor
ErrorCode is a str Enum whose members are not callable. Calling
`raise ErrorCode.SESSION_NOT_FOUND(name)` produces a TypeError
instead of the intended session-not-found error. Replace all 5
occurrences with `raise GroundingError(..., code=ErrorCode.SESSION_NOT_FOUND)`.
Closes#11
Made-with: Cursor
- Forward resolved max_iterations to grounding agent in no-skill execution path
- Use hash-based workflow ID to prevent collision across roots and separator ambiguity
Co-authored-by: wul48527-code <wul48527-code@users.noreply.github.com>
The previous __-joined scheme was not injective: a directory named
a__b and a nested path a/b both mapped to the same ID. Use a sha256
hash suffix of the resolved path instead, which is collision-free
and keeps the dir name as a human-readable prefix.
Added regression test for separator collision case.
workflow_dir.name was used as the discovery key and API ID, so two
different WORKFLOW_ROOTS containing a leaf directory with the same
name would silently drop one. Use root name + relative path joined
with __ as a stable unique ID instead.
Co-authored-by: wul48527-code <wul48527-code@users.noreply.github.com>
Without this, the no-skill path ignores the resolved max_iterations
and uses whatever default the agent has, instead of the configured
grounding_max_iterations value.
Co-authored-by: wul48527-code <wul48527-code@users.noreply.github.com>
- Add resolve() + is_relative_to() check in _extract_zip() to block
nested traversal entries like nested/../../escape.txt
- Sanitize server-provided skill name in import_skill() to prevent
directory escape via malicious record metadata
- Add 6 regression tests covering both attack vectors
Closes#17
Co-authored-by: LeftX <xzq-xu@users.noreply.github.com>
Versions 1.82.7 and 1.82.8 of litellm were published on March 24, 2026
and contained malicious code that exfiltrated credentials (SSH keys,
cloud credentials, .env files, API keys) to an attacker-controlled domain.
Pin the dependency to >=1.70.0,<1.82.7 in both pyproject.toml and
requirements.txt as a stopgap until litellm can be replaced with direct
provider SDK calls.
See: https://github.com/HKUDS/OpenSpace/issues/31
Ref: PYSEC-2026-2, BerriAI/litellm#24521