Spaces:
Configuration error
Configuration error
feat: implement /auth.md agent onboarding route and add ws broadcast improvements
Browse files- src/auth.md +111 -0
- src/functions.py +38 -0
- src/routes/health.py +26 -1
- src/viewer/index.html +84 -0
- tests/test_auth.py +58 -0
src/auth.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Agent Cache — Agentic Onboarding Instructions (`/auth.md`)
|
| 2 |
+
|
| 3 |
+
Welcome, AI Agent! You have routed to the **Agent Cache** (formerly Agent Memory) memory server.
|
| 4 |
+
|
| 5 |
+
This document serves as your guide on how to interact with this server. **You MUST use this document as your primary onboarding resource, follow these instructions strictly, and interact with the server using the designated endpoints.**
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 1. Authentication
|
| 10 |
+
|
| 11 |
+
Protected endpoints check for authentication using the `Authorization` header.
|
| 12 |
+
- **Header format**: `Authorization: Bearer <AGENTCACHE_SECRET>`
|
| 13 |
+
- If no secret is configured on the server, authentication is bypassed.
|
| 14 |
+
- The onboarding endpoint `/auth.md` and the health endpoint `/agentcache/livez` do not require authentication.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 2. Core REST API Endpoints
|
| 19 |
+
|
| 20 |
+
All endpoints support `application/json` payloads.
|
| 21 |
+
|
| 22 |
+
### Observations (Folder-scoped Memory)
|
| 23 |
+
The primary unit of storage is scoped to `(folder_path, agent_id)` pairs.
|
| 24 |
+
- **Log Observation**: `POST /agentcache/observe` (or `POST /agentmemory/observe`)
|
| 25 |
+
- **Payload**:
|
| 26 |
+
```json
|
| 27 |
+
{
|
| 28 |
+
"folderPath": "absolute/path/to/project",
|
| 29 |
+
"agentId": "your-agent-id",
|
| 30 |
+
"text": "The detailed observation text to log"
|
| 31 |
+
}
|
| 32 |
+
```
|
| 33 |
+
- **List Folders**: `GET /agentcache/folders` (or `GET /agentmemory/folders`)
|
| 34 |
+
- Returns a list of all active `(folder_path, agent_id)` pairs.
|
| 35 |
+
- **Get Observations**: `GET /agentcache/folder/observations` (or `GET /agentmemory/folder/observations`)
|
| 36 |
+
- **Parameters**: `folderPath` (string), `agentId` (string).
|
| 37 |
+
- Returns a list of all logged observations for that folder and agent.
|
| 38 |
+
|
| 39 |
+
### Global Long-term Memories
|
| 40 |
+
- **Save Global Memory**: `POST /agentcache/remember` (or `POST /agentmemory/remember`)
|
| 41 |
+
- **Payload**:
|
| 42 |
+
```json
|
| 43 |
+
{
|
| 44 |
+
"content": "The fact, pattern, or workflow to remember globally."
|
| 45 |
+
}
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Search & Retrieval
|
| 49 |
+
- **Hybrid Search**: `POST /agentcache/search` (or `POST /agentmemory/search`)
|
| 50 |
+
- Performs keyword (BM25) + vector (semantic) hybrid search.
|
| 51 |
+
- **Payload**:
|
| 52 |
+
```json
|
| 53 |
+
{
|
| 54 |
+
"query": "search term",
|
| 55 |
+
"limit": 10,
|
| 56 |
+
"folderPath": "optional filter path",
|
| 57 |
+
"agentId": "optional filter agent"
|
| 58 |
+
}
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### Activity Feed
|
| 62 |
+
- **Timeline**: `GET /agentcache/timeline` (or `GET /agentmemory/timeline`)
|
| 63 |
+
- Returns all observations and memories sorted chronologically.
|
| 64 |
+
- **Parameters**: `folderPath` (optional), `agentId` (optional), `limit` (optional).
|
| 65 |
+
|
| 66 |
+
### Forget & Clean up
|
| 67 |
+
- **Forget Item**: `POST /agentcache/forget` (or `POST /agentmemory/forget`)
|
| 68 |
+
- **Payload (Memory)**: `{"memoryId": "uuid-here"}`
|
| 69 |
+
- **Payload (Folder Pair)**: `{"folderPath": "absolute/path", "agentId": "agent-id"}`
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## 3. Model Context Protocol (MCP)
|
| 74 |
+
|
| 75 |
+
Agent Cache exposes Model Context Protocol (MCP) tools for agents equipped with MCP clients.
|
| 76 |
+
|
| 77 |
+
- **Get Tools Schema**: `GET /agentcache/mcp/tools` (or `GET /agentmemory/mcp/tools`)
|
| 78 |
+
- **Call Tool**: `POST /agentcache/mcp/tools` (or `POST /agentmemory/mcp/tools`)
|
| 79 |
+
- **Payload**:
|
| 80 |
+
```json
|
| 81 |
+
{
|
| 82 |
+
"name": "tool_name",
|
| 83 |
+
"arguments": { ... }
|
| 84 |
+
}
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Available MCP Tools
|
| 88 |
+
|
| 89 |
+
| Tool Name | Description | Required Arguments |
|
| 90 |
+
| :--- | :--- | :--- |
|
| 91 |
+
| `agent_observe` | Log observation to a folder/agent pair | `folderPath`, `agentId`, `text` |
|
| 92 |
+
| `agent_remember` | Save a global long-term memory | `content` |
|
| 93 |
+
| `cache_recall` | Query folder observations and global memories | `query` |
|
| 94 |
+
| `cache_smart_search` | Hybrid semantic + keyword search | `query` |
|
| 95 |
+
| `cache_save` | Save an important insight/decision to global memory | `content` |
|
| 96 |
+
| `cache_forget` | Delete a global memory or all observations for a pair | (one of `memoryId`, `folderPath` + `agentId`) |
|
| 97 |
+
| `cache_folders` | List all folder-agent memory scopes | None |
|
| 98 |
+
| `cache_folder_observations` | Get observations for a specific folder/agent pair | `folderPath`, `agentId` |
|
| 99 |
+
| `cache_timeline` | Retrieve activity feed of observations | None |
|
| 100 |
+
| `memory_export` | Export cache data as JSON | None |
|
| 101 |
+
| `memory_diagnose` | Run health checks and count database items | None |
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## 4. Web UI Dashboard
|
| 106 |
+
|
| 107 |
+
The visual viewer dashboard is accessible at `/viewer`. It provides tabs to inspect:
|
| 108 |
+
1. **Folders**: View folder observations list and logs.
|
| 109 |
+
2. **Memories**: Browse and search global memories.
|
| 110 |
+
3. **Graph**: Force-directed relation visualizer.
|
| 111 |
+
4. **Timeline**: Scrollable real-time chronological activity feed.
|
src/functions.py
CHANGED
|
@@ -1515,6 +1515,12 @@ def remember(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 1515 |
# Commit to Dolt
|
| 1516 |
commit_if_enabled(kv, f"Remember: {new_mem.get('title', '')}", new_mem.get("agentId"))
|
| 1517 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1518 |
return {"success": True, "memory": new_mem}
|
| 1519 |
|
| 1520 |
|
|
@@ -1559,6 +1565,11 @@ def forget(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 1559 |
_vector_index.remove(memory_id)
|
| 1560 |
deleted_mem_ids.append(memory_id)
|
| 1561 |
deleted += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1562 |
|
| 1563 |
# ------------------------------------------------------------------
|
| 1564 |
# Path 2 & 3: folder-based deletion (REQ-030, REQ-031, REQ-032, REQ-033)
|
|
@@ -1580,12 +1591,17 @@ def forget(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 1580 |
# ----------------------------------------------------------
|
| 1581 |
partial_deleted = 0
|
| 1582 |
for oid in obs_ids:
|
|
|
|
| 1583 |
existed = kv.delete(obs_scope, oid)
|
| 1584 |
if existed:
|
| 1585 |
kv.delete(KV.obs_lookup, oid)
|
| 1586 |
_bm25_index.remove(oid)
|
| 1587 |
if _vector_index:
|
| 1588 |
_vector_index.remove(oid)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1589 |
deleted_obs_ids.append(oid)
|
| 1590 |
partial_deleted += 1
|
| 1591 |
deleted += 1
|
|
@@ -1602,6 +1618,15 @@ def forget(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 1602 |
if index_entry and isinstance(index_entry, dict):
|
| 1603 |
index_entry["obsCount"] = meta["obsCount"]
|
| 1604 |
kv.set(KV.folders, index_key, index_entry)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1605 |
else:
|
| 1606 |
# ----------------------------------------------------------
|
| 1607 |
# Path 2: full pair deletion (REQ-030, REQ-032)
|
|
@@ -1624,6 +1649,19 @@ def forget(kv: StateKV, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
| 1624 |
# Remove from global folders index
|
| 1625 |
kv.delete(KV.folders, index_key)
|
| 1626 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1627 |
# ------------------------------------------------------------------
|
| 1628 |
# Legacy: session-based deletion (unchanged)
|
| 1629 |
# ------------------------------------------------------------------
|
|
|
|
| 1515 |
# Commit to Dolt
|
| 1516 |
commit_if_enabled(kv, f"Remember: {new_mem.get('title', '')}", new_mem.get("agentId"))
|
| 1517 |
|
| 1518 |
+
# Broadcast memory created
|
| 1519 |
+
broadcast_stream({
|
| 1520 |
+
"type": "memory_created",
|
| 1521 |
+
"data": new_mem,
|
| 1522 |
+
})
|
| 1523 |
+
|
| 1524 |
return {"success": True, "memory": new_mem}
|
| 1525 |
|
| 1526 |
|
|
|
|
| 1565 |
_vector_index.remove(memory_id)
|
| 1566 |
deleted_mem_ids.append(memory_id)
|
| 1567 |
deleted += 1
|
| 1568 |
+
# Broadcast memory deleted
|
| 1569 |
+
broadcast_stream({
|
| 1570 |
+
"type": "memory_deleted",
|
| 1571 |
+
"memoryId": memory_id,
|
| 1572 |
+
})
|
| 1573 |
|
| 1574 |
# ------------------------------------------------------------------
|
| 1575 |
# Path 2 & 3: folder-based deletion (REQ-030, REQ-031, REQ-032, REQ-033)
|
|
|
|
| 1591 |
# ----------------------------------------------------------
|
| 1592 |
partial_deleted = 0
|
| 1593 |
for oid in obs_ids:
|
| 1594 |
+
obs = kv.get(obs_scope, oid)
|
| 1595 |
existed = kv.delete(obs_scope, oid)
|
| 1596 |
if existed:
|
| 1597 |
kv.delete(KV.obs_lookup, oid)
|
| 1598 |
_bm25_index.remove(oid)
|
| 1599 |
if _vector_index:
|
| 1600 |
_vector_index.remove(oid)
|
| 1601 |
+
if obs and isinstance(obs, dict) and obs.get("text"):
|
| 1602 |
+
fp_text = obs["text"][:4000]
|
| 1603 |
+
dedup_fp = hashlib.sha256(fp_text.strip().lower().encode("utf-8")).hexdigest()
|
| 1604 |
+
kv.delete(KV.obs_dedup(fp, aid), dedup_fp)
|
| 1605 |
deleted_obs_ids.append(oid)
|
| 1606 |
partial_deleted += 1
|
| 1607 |
deleted += 1
|
|
|
|
| 1618 |
if index_entry and isinstance(index_entry, dict):
|
| 1619 |
index_entry["obsCount"] = meta["obsCount"]
|
| 1620 |
kv.set(KV.folders, index_key, index_entry)
|
| 1621 |
+
|
| 1622 |
+
# Broadcast observations deleted
|
| 1623 |
+
if deleted_obs_ids:
|
| 1624 |
+
broadcast_stream({
|
| 1625 |
+
"type": "observations_deleted",
|
| 1626 |
+
"folderPath": fp,
|
| 1627 |
+
"agentId": aid,
|
| 1628 |
+
"observationIds": deleted_obs_ids,
|
| 1629 |
+
})
|
| 1630 |
else:
|
| 1631 |
# ----------------------------------------------------------
|
| 1632 |
# Path 2: full pair deletion (REQ-030, REQ-032)
|
|
|
|
| 1649 |
# Remove from global folders index
|
| 1650 |
kv.delete(KV.folders, index_key)
|
| 1651 |
|
| 1652 |
+
# Clear dedup entries
|
| 1653 |
+
dedup_scope = KV.obs_dedup(fp, aid)
|
| 1654 |
+
for item in kv.list(dedup_scope):
|
| 1655 |
+
if isinstance(item, dict) and item.get("id"):
|
| 1656 |
+
kv.delete(dedup_scope, item["id"])
|
| 1657 |
+
|
| 1658 |
+
# Broadcast folder pair deleted
|
| 1659 |
+
broadcast_stream({
|
| 1660 |
+
"type": "folder_deleted",
|
| 1661 |
+
"folderPath": fp,
|
| 1662 |
+
"agentId": aid,
|
| 1663 |
+
})
|
| 1664 |
+
|
| 1665 |
# ------------------------------------------------------------------
|
| 1666 |
# Legacy: session-based deletion (unchanged)
|
| 1667 |
# ------------------------------------------------------------------
|
src/routes/health.py
CHANGED
|
@@ -9,7 +9,7 @@ Handles:
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
import os
|
| 12 |
-
from flask import Blueprint, request, jsonify
|
| 13 |
import functions
|
| 14 |
from functions import query_audit
|
| 15 |
|
|
@@ -40,6 +40,31 @@ def _get_embedding_provider():
|
|
| 40 |
return app_module.embedding_provider
|
| 41 |
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
# ---------------------------------------------------------------------------
|
| 44 |
# GET /agentcache/livez (no auth required)
|
| 45 |
# ---------------------------------------------------------------------------
|
|
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
import os
|
| 12 |
+
from flask import Blueprint, request, jsonify, Response
|
| 13 |
import functions
|
| 14 |
from functions import query_audit
|
| 15 |
|
|
|
|
| 40 |
return app_module.embedding_provider
|
| 41 |
|
| 42 |
|
| 43 |
+
# ---------------------------------------------------------------------------
|
| 44 |
+
# GET /auth.md (no auth required)
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
|
| 47 |
+
@health_bp.route("/auth.md", methods=["GET"])
|
| 48 |
+
def auth_docs():
|
| 49 |
+
"""Serve the agent onboarding documentation in Markdown format."""
|
| 50 |
+
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 51 |
+
auth_md_path = os.path.join(base_dir, "auth.md")
|
| 52 |
+
|
| 53 |
+
if os.path.exists(auth_md_path):
|
| 54 |
+
try:
|
| 55 |
+
with open(auth_md_path, "r", encoding="utf-8") as f:
|
| 56 |
+
content = f.read()
|
| 57 |
+
return Response(
|
| 58 |
+
content,
|
| 59 |
+
mimetype="text/markdown",
|
| 60 |
+
headers={"Content-Type": "text/markdown; charset=utf-8"}
|
| 61 |
+
)
|
| 62 |
+
except Exception as e:
|
| 63 |
+
return jsonify({"error": f"failed to read auth.md: {e}"}), 500
|
| 64 |
+
|
| 65 |
+
return jsonify({"error": "auth.md not found"}), 404
|
| 66 |
+
|
| 67 |
+
|
| 68 |
# ---------------------------------------------------------------------------
|
| 69 |
# GET /agentcache/livez (no auth required)
|
| 70 |
# ---------------------------------------------------------------------------
|
src/viewer/index.html
CHANGED
|
@@ -2934,6 +2934,41 @@
|
|
| 2934 |
cards.insertBefore(div.firstChild, cards.firstChild);
|
| 2935 |
}
|
| 2936 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2937 |
function updateFolderBadge(folderPath, agentId) {
|
| 2938 |
var rows = document.querySelectorAll('.folder-row');
|
| 2939 |
rows.forEach(function(row) {
|
|
@@ -3252,6 +3287,55 @@
|
|
| 3252 |
}
|
| 3253 |
if (state.activeTab === 'folders') {
|
| 3254 |
updateFolderBadge(msg.folderPath, msg.agentId);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3255 |
}
|
| 3256 |
} else if (msg.type === 'raw_observation' || msg.type === 'compressed_observation') {
|
| 3257 |
if (msg.data && msg.data.observation) {
|
|
|
|
| 2934 |
cards.insertBefore(div.firstChild, cards.firstChild);
|
| 2935 |
}
|
| 2936 |
|
| 2937 |
+
function prependFolderObservationCard(o) {
|
| 2938 |
+
var list = document.getElementById('obs-list');
|
| 2939 |
+
if (!list) return;
|
| 2940 |
+
var div = document.createElement('div');
|
| 2941 |
+
var ts = o.timestamp ? new Date(o.timestamp).toLocaleString() : '';
|
| 2942 |
+
var typeColor = {'file_edit':'var(--blue)','command_run':'var(--orange)','search':'var(--green)','error':'var(--red)','conversation':'var(--purple)'}[o.type] || 'var(--ink-muted)';
|
| 2943 |
+
var html = '';
|
| 2944 |
+
html += '<div class="obs-detail-card" data-obs-id="' + esc(o.id) + '" style="border:1px solid var(--border-light);border-radius:4px;padding:12px 16px;margin-bottom:8px;background:var(--bg);display:flex;gap:10px;align-items:flex-start;">';
|
| 2945 |
+
// checkbox
|
| 2946 |
+
html += '<label style="padding-top:2px;cursor:pointer;flex-shrink:0;">';
|
| 2947 |
+
html += '<input type="checkbox" class="obs-checkbox" data-obs-id="' + esc(o.id) + '" style="accent-color:var(--ink);">';
|
| 2948 |
+
html += '</label>';
|
| 2949 |
+
// content
|
| 2950 |
+
html += '<div style="flex:1;min-width:0;">';
|
| 2951 |
+
html += '<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap;">';
|
| 2952 |
+
html += '<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:' + typeColor + ';background:var(--bg-subtle);padding:2px 7px;border-radius:3px;">' + esc(o.type || 'other') + '</span>';
|
| 2953 |
+
html += '<span style="font-size:11px;color:var(--ink-muted);font-family:var(--font-mono);">' + esc(ts) + '</span>';
|
| 2954 |
+
html += '<span style="flex:1;"></span>';
|
| 2955 |
+
html += '<button class="btn btn-danger" data-action="delete-obs" data-obs-id="' + esc(o.id) + '" data-folder-path="' + esc(_folderDetailFp) + '" data-agent-id="' + esc(_folderDetailAid) + '" style="font-size:9px;padding:2px 8px;">Delete</button>';
|
| 2956 |
+
html += '</div>';
|
| 2957 |
+
if (o.title) html += '<div style="font-weight:600;font-size:14px;color:var(--ink);margin-bottom:4px;">' + esc(o.title) + '</div>';
|
| 2958 |
+
if (o.text) html += '<div style="font-size:13px;color:var(--ink-secondary);line-height:1.5;white-space:pre-wrap;">' + esc(o.text.length > 300 ? o.text.slice(0, 300) + '\u2026' : o.text) + '</div>';
|
| 2959 |
+
html += '</div>';
|
| 2960 |
+
html += '</div>';
|
| 2961 |
+
div.innerHTML = html;
|
| 2962 |
+
list.insertBefore(div.firstChild, list.firstChild);
|
| 2963 |
+
|
| 2964 |
+
// Update count in header
|
| 2965 |
+
var remaining = document.querySelectorAll('.obs-detail-card').length;
|
| 2966 |
+
var countEl = document.querySelector('#folder-detail-root span[style*="ink-muted"]');
|
| 2967 |
+
if (countEl) countEl.textContent = remaining + ' observation' + (remaining !== 1 ? 's' : '');
|
| 2968 |
+
|
| 2969 |
+
_wireFolderDetailCheckboxes();
|
| 2970 |
+
}
|
| 2971 |
+
|
| 2972 |
function updateFolderBadge(folderPath, agentId) {
|
| 2973 |
var rows = document.querySelectorAll('.folder-row');
|
| 2974 |
rows.forEach(function(row) {
|
|
|
|
| 3287 |
}
|
| 3288 |
if (state.activeTab === 'folders') {
|
| 3289 |
updateFolderBadge(msg.folderPath, msg.agentId);
|
| 3290 |
+
if (_folderDetailFp === msg.folderPath && _folderDetailAid === msg.agentId) {
|
| 3291 |
+
prependFolderObservationCard(msg.data || {});
|
| 3292 |
+
}
|
| 3293 |
+
}
|
| 3294 |
+
} else if (msg.type === 'memory_created' || msg.type === 'memory_deleted') {
|
| 3295 |
+
if (state.activeTab === 'memories') {
|
| 3296 |
+
state.memories.loaded = false;
|
| 3297 |
+
loadMemories();
|
| 3298 |
+
}
|
| 3299 |
+
} else if (msg.type === 'folder_deleted') {
|
| 3300 |
+
if (state.activeTab === 'folders') {
|
| 3301 |
+
if (_folderDetailFp === msg.folderPath && _folderDetailAid === msg.agentId) {
|
| 3302 |
+
loadFolders(); // go back
|
| 3303 |
+
} else {
|
| 3304 |
+
var row = document.querySelector('.folder-row[data-folder="' + esc(msg.folderPath) + '"][data-agent="' + esc(msg.agentId) + '"]');
|
| 3305 |
+
if (row) row.remove();
|
| 3306 |
+
}
|
| 3307 |
+
}
|
| 3308 |
+
} else if (msg.type === 'observations_deleted') {
|
| 3309 |
+
if (state.activeTab === 'timeline') {
|
| 3310 |
+
var oids = new Set(msg.observationIds || []);
|
| 3311 |
+
state.timeline.observations = state.timeline.observations.filter(function(o) {
|
| 3312 |
+
return !oids.has(o.id);
|
| 3313 |
+
});
|
| 3314 |
+
renderObservations();
|
| 3315 |
+
}
|
| 3316 |
+
if (state.activeTab === 'folders') {
|
| 3317 |
+
if (_folderDetailFp === msg.folderPath && _folderDetailAid === msg.agentId) {
|
| 3318 |
+
var oids = new Set(msg.observationIds || []);
|
| 3319 |
+
oids.forEach(function(oid) {
|
| 3320 |
+
var card = document.querySelector('.obs-detail-card[data-obs-id="' + oid + '"]');
|
| 3321 |
+
if (card) card.remove();
|
| 3322 |
+
_selectedObsIds.delete(oid);
|
| 3323 |
+
});
|
| 3324 |
+
var remaining = document.querySelectorAll('.obs-detail-card').length;
|
| 3325 |
+
var countEl = document.querySelector('#folder-detail-root span[style*="ink-muted"]');
|
| 3326 |
+
if (countEl) countEl.textContent = remaining + ' observation' + (remaining !== 1 ? 's' : '');
|
| 3327 |
+
} else {
|
| 3328 |
+
var rows = document.querySelectorAll('.folder-row');
|
| 3329 |
+
rows.forEach(function(row) {
|
| 3330 |
+
if (row.dataset.folder === msg.folderPath && row.dataset.agent === msg.agentId) {
|
| 3331 |
+
var countCell = row.querySelector('td:nth-child(3)');
|
| 3332 |
+
if (countCell) {
|
| 3333 |
+
var cur = parseInt(countCell.textContent) || 0;
|
| 3334 |
+
countCell.textContent = Math.max(0, cur - (msg.observationIds || []).length);
|
| 3335 |
+
}
|
| 3336 |
+
}
|
| 3337 |
+
});
|
| 3338 |
+
}
|
| 3339 |
}
|
| 3340 |
} else if (msg.type === 'raw_observation' || msg.type === 'compressed_observation') {
|
| 3341 |
if (msg.data && msg.data.observation) {
|
tests/test_auth.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
tests/test_auth.py
|
| 3 |
+
|
| 4 |
+
Tests for the /auth.md agent onboarding route.
|
| 5 |
+
"""
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
import pytest
|
| 9 |
+
|
| 10 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@pytest.fixture(scope="module")
|
| 14 |
+
def flask_app(tmp_path_factory):
|
| 15 |
+
tmp_dir = tmp_path_factory.mktemp("auth_test_db")
|
| 16 |
+
db_path = str(tmp_dir / "test.db")
|
| 17 |
+
os.environ.pop("AGENTCACHE_SECRET", None)
|
| 18 |
+
os.environ.pop("AGENTMEMORY_SECRET", None)
|
| 19 |
+
|
| 20 |
+
from db import StateKV
|
| 21 |
+
original_init = StateKV.__init__
|
| 22 |
+
|
| 23 |
+
def patched_init(self, db_path_arg=None, **kwargs):
|
| 24 |
+
original_init(self, db_path=db_path, **kwargs)
|
| 25 |
+
|
| 26 |
+
StateKV.__init__ = patched_init
|
| 27 |
+
import app as app_module
|
| 28 |
+
flask_application = app_module.create_app()
|
| 29 |
+
StateKV.__init__ = original_init
|
| 30 |
+
flask_application.config["TESTING"] = True
|
| 31 |
+
return flask_application
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@pytest.fixture(scope="module")
|
| 35 |
+
def client(flask_app):
|
| 36 |
+
return flask_app.test_client()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_auth_md_endpoint(client):
|
| 40 |
+
resp = client.get("/auth.md")
|
| 41 |
+
assert resp.status_code == 200
|
| 42 |
+
assert resp.headers["Content-Type"].startswith("text/markdown")
|
| 43 |
+
content = resp.data.decode("utf-8")
|
| 44 |
+
assert "# Agent Cache" in content
|
| 45 |
+
assert "Authorization" in content
|
| 46 |
+
assert "agent_observe" in content
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def test_auth_md_endpoint_with_secret(flask_app):
|
| 50 |
+
os.environ["AGENTCACHE_SECRET"] = "super-secret-token"
|
| 51 |
+
try:
|
| 52 |
+
client = flask_app.test_client()
|
| 53 |
+
resp = client.get("/auth.md")
|
| 54 |
+
assert resp.status_code == 200
|
| 55 |
+
content = resp.data.decode("utf-8")
|
| 56 |
+
assert "# Agent Cache" in content
|
| 57 |
+
finally:
|
| 58 |
+
os.environ.pop("AGENTCACHE_SECRET", None)
|