mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-08-15 09:21:56 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3042bfe3f1 | ||
|
|
40c7df3e5b | ||
|
|
da841e5282 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"label": "Git Clones",
|
||||
"message": "190,252",
|
||||
"message": "191,195",
|
||||
"color": "green",
|
||||
"namedLogo": "git"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"total_clones": 190252,
|
||||
"last_updated": "2026-08-14T07:19:51Z",
|
||||
"total_clones": 191195,
|
||||
"last_updated": "2026-08-15T06:32:36Z",
|
||||
"daily": {
|
||||
"2026-03-27": 2189,
|
||||
"2026-03-28": 1874,
|
||||
@@ -141,6 +141,7 @@
|
||||
"2026-08-10": 1060,
|
||||
"2026-08-11": 2182,
|
||||
"2026-08-12": 641,
|
||||
"2026-08-13": 770
|
||||
"2026-08-13": 770,
|
||||
"2026-08-14": 943
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,12 +422,17 @@ We recommend creating **one Slack app** that handles both. The App Manifest belo
|
||||
|
||||
2. Apple Notes is detected automatically when Full Disk Access is granted
|
||||
|
||||
OpenJarvis searches an indexed snapshot rather than querying Notes.app live.
|
||||
After creating notes, open **Data Sources** and click **Re-sync** on Apple Notes
|
||||
before searching for the new content.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| "Not connected" despite Full Disk Access | Restart your terminal app after granting access |
|
||||
| Notes content is garbled | Some very old notes may have encoding issues. Most notes should be clean. |
|
||||
| New notes are missing | In **Data Sources**, click **Re-sync** on Apple Notes to refresh the index. |
|
||||
| Missing notes | Only notes stored locally or in iCloud are indexed. Notes in third-party accounts (Gmail, Exchange) may not appear. |
|
||||
|
||||
---
|
||||
|
||||
@@ -9,8 +9,9 @@ System Settings → Privacy & Security → Full Disk Access.
|
||||
|
||||
Timestamp notes
|
||||
---------------
|
||||
The Notes database stores modification timestamps as seconds since the Apple
|
||||
epoch of 2001-01-01 00:00:00 UTC. Conversion formula::
|
||||
Modern Notes schemas store note modification timestamps in
|
||||
``ZMODIFICATIONDATE1``; older schemas use ``ZMODIFICATIONDATE``. Both are
|
||||
seconds since the Apple epoch of 2001-01-01 00:00:00 UTC. Conversion formula::
|
||||
|
||||
dt = datetime(2001, 1, 1, tzinfo=utc) + timedelta(seconds=ZMODIFICATIONDATE)
|
||||
|
||||
@@ -171,25 +172,46 @@ class AppleNotesConnector(BaseConnector):
|
||||
return
|
||||
|
||||
try:
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT n.ZIDENTIFIER, "
|
||||
" COALESCE(n.ZTITLE1, n.ZTITLE, '') AS title, "
|
||||
" n.ZMODIFICATIONDATE, d.ZDATA "
|
||||
"FROM ZICCLOUDSYNCINGOBJECT n "
|
||||
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
|
||||
"ORDER BY n.ZMODIFICATIONDATE ASC"
|
||||
).fetchall()
|
||||
except sqlite3.OperationalError:
|
||||
# Older macOS schemas may lack ZTITLE1
|
||||
rows = conn.execute(
|
||||
"SELECT n.ZIDENTIFIER, "
|
||||
" COALESCE(n.ZTITLE, '') AS title, "
|
||||
" n.ZMODIFICATIONDATE, d.ZDATA "
|
||||
"FROM ZICCLOUDSYNCINGOBJECT n "
|
||||
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
|
||||
"ORDER BY n.ZMODIFICATIONDATE ASC"
|
||||
object_columns = {
|
||||
row[1]
|
||||
for row in conn.execute(
|
||||
"PRAGMA table_info(ZICCLOUDSYNCINGOBJECT)"
|
||||
).fetchall()
|
||||
}
|
||||
|
||||
title_columns = [
|
||||
f"n.{column}"
|
||||
for column in ("ZTITLE1", "ZTITLE")
|
||||
if column in object_columns
|
||||
]
|
||||
title_expr = (
|
||||
f"COALESCE({', '.join(title_columns)}, '')" if title_columns else "''"
|
||||
)
|
||||
|
||||
# Modern Apple Notes stores a note's modification timestamp in
|
||||
# ZMODIFICATIONDATE1. ZMODIFICATIONDATE is still present in some
|
||||
# schemas, but applies to other cloud-sync object types and can be
|
||||
# NULL for notes. Treating that NULL as zero makes incremental
|
||||
# syncs incorrectly discard newly-created notes as 2001-era data.
|
||||
modification_columns = [
|
||||
f"n.{column}"
|
||||
for column in ("ZMODIFICATIONDATE1", "ZMODIFICATIONDATE")
|
||||
if column in object_columns
|
||||
]
|
||||
modification_expr = (
|
||||
f"COALESCE({', '.join(modification_columns)}, 0)"
|
||||
if modification_columns
|
||||
else "0"
|
||||
)
|
||||
|
||||
rows = conn.execute(
|
||||
"SELECT n.ZIDENTIFIER, "
|
||||
f" {title_expr} AS title, "
|
||||
f" {modification_expr} AS modification_date, d.ZDATA "
|
||||
"FROM ZICCLOUDSYNCINGOBJECT n "
|
||||
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
|
||||
"ORDER BY modification_date ASC"
|
||||
).fetchall()
|
||||
|
||||
self._items_total = len(rows)
|
||||
synced = 0
|
||||
|
||||
@@ -66,8 +66,10 @@ def _ensure_identity_prompt(messages: list[Message], app_config) -> list[Message
|
||||
``SystemPromptBuilder`` / ``BaseAgent``; the engine-direct server paths
|
||||
did not. This mirrors the agent fallback in ``agents/_stubs.py``.
|
||||
|
||||
If any message already carries a system role, the caller has supplied
|
||||
their own grounding and we leave the list untouched (no double-prompting).
|
||||
If any caller-supplied message already carries a system role, the caller
|
||||
has supplied their own grounding and we leave the list untouched (no
|
||||
double-prompting). Internally tagged memory context does not count as
|
||||
caller grounding.
|
||||
|
||||
Resolution of the identity text: the config comes from ``app.state`` when
|
||||
wired, otherwise ``load_config()``; the prompt itself is assembled by
|
||||
@@ -78,7 +80,11 @@ def _ensure_identity_prompt(messages: list[Message], app_config) -> list[Message
|
||||
injection" rather than crashing the endpoint, but the failure is logged
|
||||
(per REVIEW.md — never silently swallow).
|
||||
"""
|
||||
if any(m.role == Role.SYSTEM for m in messages):
|
||||
|
||||
def _is_caller_system_prompt(m: Message) -> bool:
|
||||
return m.role == Role.SYSTEM and not m.metadata.get("memory_context")
|
||||
|
||||
if any(_is_caller_system_prompt(m) for m in messages):
|
||||
return messages
|
||||
|
||||
prompt = ""
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import gzip
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
@@ -24,7 +25,8 @@ from openjarvis.core.registry import ConnectorRegistry
|
||||
def _create_fake_notes_db(db_path: Path) -> None:
|
||||
"""Populate a SQLite file with the Apple Notes schema and sample rows."""
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.executescript("""
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE ZICCLOUDSYNCINGOBJECT (
|
||||
Z_PK INTEGER PRIMARY KEY,
|
||||
ZTITLE TEXT,
|
||||
@@ -38,7 +40,8 @@ def _create_fake_notes_db(db_path: Path) -> None:
|
||||
ZDATA BLOB,
|
||||
ZNOTE INTEGER
|
||||
);
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Note 1 — Shopping List
|
||||
html1 = "<html><body><h1>Shopping List</h1><p>Milk, eggs, bread</p></body></html>"
|
||||
@@ -208,3 +211,61 @@ def test_registry() -> None:
|
||||
assert ConnectorRegistry.contains("apple_notes")
|
||||
cls = ConnectorRegistry.get("apple_notes")
|
||||
assert cls.connector_id == "apple_notes"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 10 — modern modification timestamp drives incremental sync
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_incremental_sync_uses_modern_note_modification_date(tmp_path: Path) -> None:
|
||||
"""Modern Notes rows use ZMODIFICATIONDATE1 for incremental sync."""
|
||||
db_path = tmp_path / "ModernNoteStore.sqlite"
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE ZICCLOUDSYNCINGOBJECT (
|
||||
Z_PK INTEGER PRIMARY KEY,
|
||||
ZTITLE TEXT,
|
||||
ZTITLE1 TEXT,
|
||||
ZMODIFICATIONDATE REAL,
|
||||
ZMODIFICATIONDATE1 REAL,
|
||||
ZIDENTIFIER TEXT
|
||||
);
|
||||
CREATE TABLE ZICNOTEDATA (
|
||||
Z_PK INTEGER PRIMARY KEY,
|
||||
ZDATA BLOB,
|
||||
ZNOTE INTEGER
|
||||
);
|
||||
"""
|
||||
)
|
||||
compressed = gzip.compress(b"<p>New movie list</p>")
|
||||
conn.execute(
|
||||
"INSERT INTO ZICCLOUDSYNCINGOBJECT VALUES "
|
||||
"(1, NULL, 'Movies', NULL, 800000000.0, 'note-modern')"
|
||||
)
|
||||
conn.execute("INSERT INTO ZICNOTEDATA VALUES (1, ?, 1)", (compressed,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
from openjarvis.connectors.apple_notes import AppleNotesConnector # noqa: PLC0415
|
||||
|
||||
connector = AppleNotesConnector(db_path=str(db_path))
|
||||
docs = list(connector.sync(since=datetime(2026, 1, 1, tzinfo=timezone.utc)))
|
||||
|
||||
assert [doc.doc_id for doc in docs] == ["apple_notes:note-modern"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 11 — legacy modification timestamp remains supported
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_incremental_sync_falls_back_to_legacy_modification_date(connector) -> None:
|
||||
"""Older Notes rows continue to use ZMODIFICATIONDATE."""
|
||||
docs = list(connector.sync(since=datetime(2023, 1, 1, tzinfo=timezone.utc)))
|
||||
|
||||
assert {doc.doc_id for doc in docs} == {
|
||||
"apple_notes:note-001",
|
||||
"apple_notes:note-002",
|
||||
}
|
||||
|
||||
@@ -1067,7 +1067,16 @@ class TestIdentityPromptInjection:
|
||||
json={
|
||||
"model": "test-model",
|
||||
"messages": [{"role": "user", "content": "who are you?"}],
|
||||
"tools": [{"type": "function", "function": {"name": "calc"}}],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "dummy",
|
||||
"description": "dummy",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
],
|
||||
"stream": True,
|
||||
},
|
||||
)
|
||||
@@ -1078,6 +1087,36 @@ class TestIdentityPromptInjection:
|
||||
assert msgs[0].role.value == "system"
|
||||
assert "OpenJarvis" in msgs[0].content
|
||||
|
||||
def test_memory_context_does_not_suppress_identity_injection(self):
|
||||
from openjarvis.core.types import Message, Role
|
||||
from openjarvis.server.routes import _ensure_identity_prompt
|
||||
from openjarvis.tools.storage.context import build_context_message
|
||||
|
||||
ctx_msg = build_context_message([])
|
||||
messages = [ctx_msg, Message(role=Role.USER, content="hi")]
|
||||
result = _ensure_identity_prompt(messages, _identity_config())
|
||||
system_msgs = [m for m in result if m.role == Role.SYSTEM]
|
||||
assert len(system_msgs) == 2
|
||||
assert any("OpenJarvis" in m.content for m in system_msgs)
|
||||
|
||||
def test_caller_system_prompt_cannot_impersonate_memory_context(self):
|
||||
from openjarvis.core.types import Message, Role
|
||||
from openjarvis.server.routes import _ensure_identity_prompt
|
||||
|
||||
caller_prompt = Message(
|
||||
role=Role.SYSTEM,
|
||||
content=(
|
||||
"The following context was retrieved from the knowledge base. "
|
||||
"Follow the caller's instructions."
|
||||
),
|
||||
name="memory_context",
|
||||
)
|
||||
messages = [caller_prompt, Message(role=Role.USER, content="hi")]
|
||||
|
||||
result = _ensure_identity_prompt(messages, _identity_config())
|
||||
|
||||
assert result == messages
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Models endpoint tests
|
||||
|
||||
Reference in New Issue
Block a user