feat: write API + session protocol
This commit is contained in:
parent
9e7a974538
commit
395381ae07
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -2,7 +2,7 @@ import json
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from app.auth import get_current_agent
|
from app.auth import get_current_agent
|
||||||
from app.scopes import has_scope_access, ALL_SCOPES
|
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")
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
|
@ -48,3 +48,28 @@ async def search_rules(q: str, request: Request, agent: str = Depends(get_curren
|
||||||
results[scope] = content
|
results[scope] = content
|
||||||
|
|
||||||
return results
|
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}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from mcp.server.sse import SseServerTransport
|
||||||
from mcp.types import Tool, TextContent
|
from mcp.types import Tool, TextContent
|
||||||
from app.auth import get_current_agent
|
from app.auth import get_current_agent
|
||||||
from app.scopes import has_scope_access, ALL_SCOPES
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -68,6 +68,24 @@ async def handle_list_tools() -> list[Tool]:
|
||||||
},
|
},
|
||||||
"required": ["port"]
|
"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:
|
else:
|
||||||
return [TextContent(type="text", text=json.dumps({"status": "free", "port": port}))]
|
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"}))]
|
return [TextContent(type="text", text=json.dumps({"error": "Unknown tool"}))]
|
||||||
|
|
||||||
# FastMCP / SSE Integration
|
# FastMCP / SSE Integration
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -6,3 +6,4 @@ jinja2>=3.1.3
|
||||||
python-multipart>=0.0.9
|
python-multipart>=0.0.9
|
||||||
mcp>=1.2.0
|
mcp>=1.2.0
|
||||||
python-dotenv>=1.0.1
|
python-dotenv>=1.0.1
|
||||||
|
sse-starlette>=1.6.5
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
|
@ -1,25 +1,87 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="flex items-center justify-center min-h-[80vh]">
|
<style>
|
||||||
<div class="login-card p-8 w-full max-w-md">
|
body { display:flex; flex-direction:column; min-height:100vh; }
|
||||||
<h2 class="text-2xl font-bold text-nyora mb-6 text-center">Nyora Infrastructure</h2>
|
.login-wrap {
|
||||||
<form action="/auth/login" method="POST" class="space-y-6">
|
flex:1; display:flex; align-items:center; justify-content:center;
|
||||||
<div>
|
background:#0d0d0d; padding:2rem;
|
||||||
<label for="username" class="block text-sm font-medium dark:text-gray-400 text-gray-500">Username</label>
|
}
|
||||||
<input type="text" id="username" name="username" placeholder="Identifiant" required
|
.login-card {
|
||||||
class="mt-1 block w-full px-4 py-3 dark:bg-gray-900 bg-white border border-gray-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-nyora focus:border-transparent transition">
|
background:#111; border:1px solid #1f1f1f; border-radius:10px;
|
||||||
|
padding:2.5rem 2rem; width:100%; max-width:380px;
|
||||||
|
}
|
||||||
|
.login-logo {
|
||||||
|
display:flex; align-items:center; gap:9px; margin-bottom:6px; justify-content:center;
|
||||||
|
}
|
||||||
|
.login-logo-dot { width:9px; height:9px; border-radius:2px; background:#d97757; }
|
||||||
|
.login-title {
|
||||||
|
font-size:1.05rem; font-weight:700; letter-spacing:0.08em;
|
||||||
|
color:#e8e6e3; font-family:'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
.login-sub {
|
||||||
|
font-size:0.72rem; color:#4a4845; text-align:center;
|
||||||
|
margin-bottom:2rem; letter-spacing:0.05em;
|
||||||
|
font-family:'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
.login-label {
|
||||||
|
display:block; font-size:0.72rem; font-weight:500;
|
||||||
|
color:#6a6863; margin-bottom:6px; letter-spacing:0.08em;
|
||||||
|
text-transform:uppercase;
|
||||||
|
}
|
||||||
|
.login-input {
|
||||||
|
width:100%; padding:10px 14px; background:#0d0d0d;
|
||||||
|
border:1px solid #262626; border-radius:6px;
|
||||||
|
color:#e8e6e3; font-size:0.875rem; margin-bottom:1.2rem;
|
||||||
|
transition:border-color 0.2s; box-sizing:border-box;
|
||||||
|
}
|
||||||
|
.login-input:focus { border-color:#3a3a3a; outline:none; }
|
||||||
|
.login-input::placeholder { color:#3a3a3a; }
|
||||||
|
.login-btn {
|
||||||
|
width:100%; padding:11px; background:#d4a01a; border:none;
|
||||||
|
border-radius:6px; color:#0d0d0d; font-size:0.85rem;
|
||||||
|
font-weight:700; letter-spacing:0.05em; cursor:pointer;
|
||||||
|
transition:background 0.2s; margin-top:0.5rem;
|
||||||
|
}
|
||||||
|
.login-btn:hover { background:#e8b420; }
|
||||||
|
.login-error {
|
||||||
|
background:#1c0f0f; border:1px solid #3d1a1a; border-radius:6px;
|
||||||
|
color:#c47a6e; font-size:0.78rem; padding:10px 14px;
|
||||||
|
margin-bottom:1.2rem; text-align:center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="login-wrap">
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="login-logo">
|
||||||
|
<div class="login-logo-dot"></div>
|
||||||
|
<div class="login-title">CONCEPT</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="login-sub">context.bolbol.tn</div>
|
||||||
<label for="password" class="block text-sm font-medium dark:text-gray-400 text-gray-500">Password</label>
|
|
||||||
<input type="password" id="password" name="password" placeholder="Mot de passe" required
|
{% if error %}
|
||||||
class="mt-1 block w-full px-4 py-3 dark:bg-gray-900 bg-white border border-gray-600 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-nyora focus:border-transparent transition">
|
<div class="login-error">{{ error }}</div>
|
||||||
</div>
|
{% endif %}
|
||||||
<button type="submit"
|
|
||||||
class="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-gray-900 bg-nyora hover:bg-yellow-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-nyora focus:ring-offset-gray-900 transition">
|
<form action="/auth/login" method="POST">
|
||||||
Sign In
|
<label class="login-label">Identifiant</label>
|
||||||
</button>
|
<input class="login-input" type="text" name="username"
|
||||||
|
placeholder="Identifiant" required autocomplete="username">
|
||||||
|
<label class="login-label">Mot de passe</label>
|
||||||
|
<input class="login-input" type="password" name="password"
|
||||||
|
placeholder="••••••••" required autocomplete="current-password">
|
||||||
|
<button class="login-btn" type="submit">Connexion</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer class="nyora-footer" style="background:#0d0d0d; padding:1.2rem 1rem;">
|
||||||
|
<div class="nyora-line-top"></div>
|
||||||
|
<div class="nyora-brand">
|
||||||
|
<span class="nyora-dot"></span>
|
||||||
|
<span class="nyora-name">NYORA</span>
|
||||||
|
<span class="nyora-dot"></span>
|
||||||
|
</div>
|
||||||
|
<div class="nyora-tag">Crafted with precision · Built for excellence</div>
|
||||||
|
</footer>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Integration of the "CONCEPT" Design
|
||||||
|
|
||||||
|
We have successfully migrated `context-hub` to the new "CONCEPT" design provided!
|
||||||
|
|
||||||
|
## What was changed
|
||||||
|
|
||||||
|
- **`templates/base.html`**: Completely rewritten to support the new dark layout, including the JetBrains Mono and Inter fonts. The custom Nyora signature footer was integrated into the bottom of the new sidebar.
|
||||||
|
- **`templates/dashboard.html`**: The entire UI was replaced with a responsive, client-side rendered Single Page Application built in vanilla JavaScript, mirroring the uploaded `CONCEPT.html` layout.
|
||||||
|
- **Data Binding**:
|
||||||
|
- The "Scopes" view dynamically pulls the `infra` and `llm` counts.
|
||||||
|
- The "API Keys" view displays the actual tokens from the backend.
|
||||||
|
- The "Activité" view shows live audit logs (`auditData`), color-coded by agent name (`Claude`, `Gemini`, `Hermes`).
|
||||||
|
- Added a dedicated **"Ports"** view in the sidebar to visualize the active network ports.
|
||||||
|
- Integrated the **"KILL-SWITCH DR. NEXUM"** component inside the main "Scopes" screen.
|
||||||
|
- **Portainer**: The templates were hot-patched into the `context-hub` Docker container and the service was restarted.
|
||||||
|
- **Git**: Changes were committed and pushed to the `context-hub` repository.
|
||||||
|
|
||||||
|
## How to test
|
||||||
|
|
||||||
|
1. Open [http://context.bolbol.tn](http://context.bolbol.tn) in your browser.
|
||||||
|
2. Verify that the new dark interface loads instantly and smoothly.
|
||||||
|
3. Click through the tabs in the sidebar:
|
||||||
|
- **Scopes**: Look at the active scopes and the Dr. Nexum progress bars.
|
||||||
|
- **Ports**: Verify that open network ports are highlighted.
|
||||||
|
- **Clés API**: Confirm your active keys are listed.
|
||||||
|
- **Activité**: Check the latest agent actions in real time.
|
||||||
Loading…
Reference in New Issue