diff --git a/@eaDir/walkthrough.2.md@SynoEAStream b/@eaDir/walkthrough.2.md@SynoEAStream new file mode 100644 index 0000000..1ca6728 Binary files /dev/null and b/@eaDir/walkthrough.2.md@SynoEAStream differ diff --git a/app/__pycache__/main.cpython-311.pyc b/app/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..f2cbfb1 Binary files /dev/null and b/app/__pycache__/main.cpython-311.pyc differ diff --git a/app/routes/admin.py b/app/routes/admin.py index ec1b612..c965b25 100644 Binary files a/app/routes/admin.py and b/app/routes/admin.py differ diff --git a/app/routes/api.py b/app/routes/api.py index 54a5e62..3858cfb 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -2,7 +2,7 @@ import json from fastapi import APIRouter, Depends, HTTPException, Request from app.auth import get_current_agent from app.scopes import has_scope_access, ALL_SCOPES -from app.models import get_rule, get_all_rules, get_all_ports, add_audit_log +from app.models import get_rule, get_all_rules, get_all_ports, add_audit_log, set_rule router = APIRouter(prefix="/api") @@ -48,3 +48,28 @@ async def search_rules(q: str, request: Request, agent: str = Depends(get_curren results[scope] = content return results + +@router.patch("/rules/{scope}") +async def patch_scope_rules(scope: str, request: Request, agent: str = Depends(get_current_agent)): + if not has_scope_access(agent, scope): + await log_audit(request, agent, f"{scope} (WRITE FORBIDDEN)") + raise HTTPException(status_code=403, detail="Forbidden") + body = await request.json() + if not isinstance(body, dict): + raise HTTPException(status_code=400, detail="Body must be a JSON object") + existing = await get_rule(scope) or {} + existing.update(body) + await set_rule(scope, existing) + await log_audit(request, agent, f"{scope} (WRITE)") + return {"status": "ok", "scope": scope, "updated_keys": list(body.keys())} + +@router.get("/context") +async def get_agent_context(request: Request, agent: str = Depends(get_current_agent)): + await log_audit(request, agent, "context (READ ALL)") + all_rules = await get_all_rules() + result = {} + for scope in ALL_SCOPES: + if has_scope_access(agent, scope): + result[scope] = all_rules.get(scope, {}) + ports = await get_all_ports() + return {"agent": agent, "scopes": result, "ports": ports} diff --git a/app/routes/mcp.py b/app/routes/mcp.py index e05fd87..ae671fd 100644 --- a/app/routes/mcp.py +++ b/app/routes/mcp.py @@ -7,7 +7,7 @@ from mcp.server.sse import SseServerTransport from mcp.types import Tool, TextContent from app.auth import get_current_agent from app.scopes import has_scope_access, ALL_SCOPES -from app.models import get_rule, get_all_rules, get_all_ports, get_port +from app.models import get_rule, get_all_rules, get_all_ports, get_port, set_rule logger = logging.getLogger(__name__) @@ -68,6 +68,24 @@ async def handle_list_tools() -> list[Tool]: }, "required": ["port"] } + ), + Tool( + name="get_my_context", + description="Get all scopes authorized for this agent — call at START of session", + inputSchema={"type": "object", "properties": {"agent": {"type": "string"}}, "required": ["agent"]} + ), + Tool( + name="update_rule", + description="Merge-update a scope with new key-value data — call at END of session", + inputSchema={ + "type": "object", + "properties": { + "scope": {"type": "string", "description": "Scope to update (infra/llm/nyora/perso/tt)"}, + "agent": {"type": "string", "description": "Agent name for scope access check"}, + "data": {"type": "object", "description": "Key-value pairs to merge into the scope"} + }, + "required": ["scope", "agent", "data"] + } ) ] @@ -124,6 +142,28 @@ async def handle_call_tool(name: str, arguments: dict) -> list[TextContent]: else: return [TextContent(type="text", text=json.dumps({"status": "free", "port": port}))] + elif name == "get_my_context": + agent_name = arguments.get("agent", "") + all_rules = await get_all_rules() + result = {} + for scope in ALL_SCOPES: + if has_scope_access(agent_name, scope): + result[scope] = all_rules.get(scope, {}) + return [TextContent(type="text", text=json.dumps({"agent": agent_name, "scopes": result}))] + + elif name == "update_rule": + scope = arguments.get("scope") + agent_name = arguments.get("agent", "") + data = arguments.get("data", {}) + if not has_scope_access(agent_name, scope): + return [TextContent(type="text", text=json.dumps({"error": f"Agent {agent_name} forbidden on scope {scope}"}))] + if not isinstance(data, dict): + return [TextContent(type="text", text=json.dumps({"error": "data must be a JSON object"}))] + existing = await get_rule(scope) or {} + existing.update(data) + await set_rule(scope, existing) + return [TextContent(type="text", text=json.dumps({"status": "ok", "scope": scope, "updated_keys": list(data.keys())}))] + return [TextContent(type="text", text=json.dumps({"error": "Unknown tool"}))] # FastMCP / SSE Integration diff --git a/data/context_hub.db b/data/context_hub.db index 46ce0d8..f09b5a9 100644 Binary files a/data/context_hub.db and b/data/context_hub.db differ diff --git a/requirements.txt b/requirements.txt index c365fdd..b8a8535 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ jinja2>=3.1.3 python-multipart>=0.0.9 mcp>=1.2.0 python-dotenv>=1.0.1 +sse-starlette>=1.6.5 diff --git a/templates/CONCEPT.html b/templates/CONCEPT.html new file mode 100644 index 0000000..ae40977 --- /dev/null +++ b/templates/CONCEPT.html @@ -0,0 +1,186 @@ + + +
+ +