Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32ced8bc1a | ||
|
|
c9ae60d1ab | ||
|
|
31f4d70471 | ||
|
|
8444210d91 | ||
|
|
f2dda679eb | ||
|
|
85e83ebb35 | ||
|
|
521dca85a4 | ||
|
|
fcc3c5132c | ||
|
|
7569386e56 |
@@ -0,0 +1,56 @@
|
|||||||
|
"""API adapter for agent context routing and visibility."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from fastapi import APIRouter
|
||||||
|
except Exception:
|
||||||
|
APIRouter = None
|
||||||
|
|
||||||
|
from .agent_context_dashboard import AgentContextDashboard
|
||||||
|
from .agent_context_rbac import AgentContextRBAC
|
||||||
|
from .agent_context_resolver import AgentContextResolver
|
||||||
|
|
||||||
|
resolver = AgentContextResolver()
|
||||||
|
rbac = AgentContextRBAC()
|
||||||
|
dashboard = AgentContextDashboard()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_agent_context(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
route = rbac.route(payload["user_id"], payload.get("group_id"), payload.get("role", "user"))
|
||||||
|
context = resolver.resolve(
|
||||||
|
user_id=payload["user_id"],
|
||||||
|
group_id=payload.get("group_id", ""),
|
||||||
|
session=payload.get("session"),
|
||||||
|
user_profile=payload.get("user_profile"),
|
||||||
|
group_policy=payload.get("group_policy"),
|
||||||
|
agent_default=payload.get("agent_default"),
|
||||||
|
owner_policy=payload.get("owner_policy"),
|
||||||
|
owner=payload.get("role") == "owner",
|
||||||
|
)
|
||||||
|
return {"routing": route.to_dict(), "context": context.to_dict()}
|
||||||
|
|
||||||
|
|
||||||
|
def get_context_dashboard() -> dict[str, Any]:
|
||||||
|
return dashboard.overview()
|
||||||
|
|
||||||
|
|
||||||
|
def build_router():
|
||||||
|
if APIRouter is None:
|
||||||
|
return None
|
||||||
|
router = APIRouter(prefix="/admin/ai-evolution/context", tags=["context-isolation"])
|
||||||
|
|
||||||
|
@router.post("/resolve", operation_id="resolveAgentContext")
|
||||||
|
def resolve(payload: dict[str, Any]):
|
||||||
|
return resolve_agent_context(payload)
|
||||||
|
|
||||||
|
@router.get("/overview", operation_id="getAgentContextDashboard")
|
||||||
|
def overview():
|
||||||
|
return get_context_dashboard()
|
||||||
|
|
||||||
|
return router
|
||||||
|
|
||||||
|
|
||||||
|
router = build_router()
|
||||||
|
|
||||||
|
__all__ = ["router", "resolve_agent_context", "get_context_dashboard"]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""Audit context resolution decisions."""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now():
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
class AgentContextAudit:
|
||||||
|
def record(self, context: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"event": "context_loaded",
|
||||||
|
"agent_id": context.get("agent_id"),
|
||||||
|
"user_id": context.get("user_id"),
|
||||||
|
"group_id": context.get("group_id"),
|
||||||
|
"layers": [x.get("name") for x in context.get("layers", [])],
|
||||||
|
"timestamp": utc_now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["AgentContextAudit"]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""Web UI model for context isolation visibility."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class AgentContextDashboard:
|
||||||
|
def overview(self, contexts: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||||
|
contexts = contexts or []
|
||||||
|
return {
|
||||||
|
"module": "agent-context-isolation",
|
||||||
|
"route": "/admin/ai-evolution/context",
|
||||||
|
"title": "Agent Context Isolation",
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"name": "agent-routing",
|
||||||
|
"title": "Agent 路由",
|
||||||
|
"data": contexts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "context-layers",
|
||||||
|
"title": "Context 来源层级",
|
||||||
|
"layers": [
|
||||||
|
"safety_policy",
|
||||||
|
"owner_policy",
|
||||||
|
"group_policy",
|
||||||
|
"role_policy",
|
||||||
|
"user_profile",
|
||||||
|
"session_context",
|
||||||
|
"agent_default",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "audit",
|
||||||
|
"title": "Context 审计",
|
||||||
|
"events": [
|
||||||
|
"context_loaded",
|
||||||
|
"profile_updated",
|
||||||
|
"agent_selected",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["AgentContextDashboard"]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""RBAC binding for group main agents and owner agents."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AgentRoutingDecision:
|
||||||
|
user_id: str
|
||||||
|
group_id: str
|
||||||
|
role: str
|
||||||
|
agent_id: str
|
||||||
|
scope: str
|
||||||
|
allowed: bool
|
||||||
|
reason: str
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
class AgentContextRBAC:
|
||||||
|
"""Routes users to group agents and owners to dedicated owner agents."""
|
||||||
|
|
||||||
|
VALID_ROLES = {"user", "group_admin", "owner"}
|
||||||
|
|
||||||
|
def route(self, user_id: str, group_id: str | None, role: str) -> AgentRoutingDecision:
|
||||||
|
if role not in self.VALID_ROLES:
|
||||||
|
return AgentRoutingDecision(user_id, group_id or "", role, "", "none", False, "unknown_role")
|
||||||
|
if role == "owner":
|
||||||
|
return AgentRoutingDecision(
|
||||||
|
user_id=user_id,
|
||||||
|
group_id=group_id or "global",
|
||||||
|
role=role,
|
||||||
|
agent_id=f"owner-agent:{user_id}",
|
||||||
|
scope="global",
|
||||||
|
allowed=True,
|
||||||
|
reason="owner_dedicated_agent",
|
||||||
|
)
|
||||||
|
if not group_id:
|
||||||
|
return AgentRoutingDecision(user_id, "", role, "", "none", False, "group_required")
|
||||||
|
scope = "group" if role == "group_admin" else "self"
|
||||||
|
return AgentRoutingDecision(
|
||||||
|
user_id=user_id,
|
||||||
|
group_id=group_id,
|
||||||
|
role=role,
|
||||||
|
agent_id=f"group-main-agent:{group_id}",
|
||||||
|
scope=scope,
|
||||||
|
allowed=True,
|
||||||
|
reason="group_agent_routing",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["AgentRoutingDecision", "AgentContextRBAC"]
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Multi-user agent context resolver.
|
||||||
|
|
||||||
|
Architecture:
|
||||||
|
- shared agent kernel
|
||||||
|
- group main agent
|
||||||
|
- owner dedicated agent
|
||||||
|
- user profile overlay
|
||||||
|
- session overlay
|
||||||
|
|
||||||
|
Personal preferences never enter shared USER.md.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now() -> str:
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ContextLayer:
|
||||||
|
name: str
|
||||||
|
priority: int
|
||||||
|
data: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EffectiveContext:
|
||||||
|
agent_id: str
|
||||||
|
user_id: str
|
||||||
|
group_id: str
|
||||||
|
layers: list[ContextLayer]
|
||||||
|
resolved: dict[str, Any]
|
||||||
|
resolved_at: str = field(default_factory=utc_now)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"agent_id": self.agent_id,
|
||||||
|
"user_id": self.user_id,
|
||||||
|
"group_id": self.group_id,
|
||||||
|
"layers": [asdict(x) for x in self.layers],
|
||||||
|
"resolved": self.resolved,
|
||||||
|
"resolved_at": self.resolved_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AgentContextResolver:
|
||||||
|
"""Resolves context without shared user-state pollution."""
|
||||||
|
|
||||||
|
def resolve(
|
||||||
|
self,
|
||||||
|
user_id: str,
|
||||||
|
group_id: str,
|
||||||
|
session: dict[str, Any] | None = None,
|
||||||
|
user_profile: dict[str, Any] | None = None,
|
||||||
|
group_policy: dict[str, Any] | None = None,
|
||||||
|
agent_default: dict[str, Any] | None = None,
|
||||||
|
owner_policy: dict[str, Any] | None = None,
|
||||||
|
owner: bool = False,
|
||||||
|
) -> EffectiveContext:
|
||||||
|
layers = [
|
||||||
|
ContextLayer("agent_default", 10, agent_default or {}),
|
||||||
|
ContextLayer("group_policy", 30, group_policy or {}),
|
||||||
|
ContextLayer("user_profile", 50, user_profile or {}),
|
||||||
|
ContextLayer("session", 70, session or {}),
|
||||||
|
]
|
||||||
|
agent_id = f"group-main-agent:{group_id}"
|
||||||
|
if owner:
|
||||||
|
agent_id = f"owner-agent:{user_id}"
|
||||||
|
layers.append(ContextLayer("owner_policy", 90, owner_policy or {}))
|
||||||
|
resolved: dict[str, Any] = {}
|
||||||
|
# Higher priority overlays lower priority except safety policies handled upstream.
|
||||||
|
for layer in sorted(layers, key=lambda x: x.priority):
|
||||||
|
resolved.update(layer.data)
|
||||||
|
return EffectiveContext(agent_id, user_id, group_id, layers, resolved)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["ContextLayer", "EffectiveContext", "AgentContextResolver"]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
agent_context_isolation:
|
||||||
|
enabled: true
|
||||||
|
architecture:
|
||||||
|
shared_kernel: true
|
||||||
|
group_main_agent: true
|
||||||
|
owner_dedicated_agent: true
|
||||||
|
user_dedicated_agent: false
|
||||||
|
layers:
|
||||||
|
- system_policy
|
||||||
|
- agent_default
|
||||||
|
- group_policy
|
||||||
|
- role_policy
|
||||||
|
- user_profile
|
||||||
|
- session_context
|
||||||
|
priority:
|
||||||
|
session_context: 70
|
||||||
|
user_profile: 50
|
||||||
|
group_policy: 30
|
||||||
|
agent_default: 10
|
||||||
|
safety_policy: immutable
|
||||||
|
forbidden:
|
||||||
|
- user_preferences_in_USER_md
|
||||||
|
- cross_user_profile_access
|
||||||
|
- shared_personalization_state
|
||||||
|
audit:
|
||||||
|
enabled: true
|
||||||
|
events:
|
||||||
|
- context_loaded
|
||||||
|
- profile_updated
|
||||||
|
- agent_selected
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# CMD-20260709-enable-group-owner-agent-isolation
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Deploy multi-user agent context isolation:
|
||||||
|
|
||||||
|
- group main agent per group/tenant
|
||||||
|
- dedicated owner agent per owner
|
||||||
|
- user profile and session overlays
|
||||||
|
- no user personalization in shared USER.md
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
1. Deploy context resolver.
|
||||||
|
2. Enable group agent routing.
|
||||||
|
3. Enable owner agent routing.
|
||||||
|
4. Create runtime user profile storage.
|
||||||
|
5. Migrate user-specific preferences out of shared USER.md.
|
||||||
|
6. Enable context audit events.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- AIOS_USER_PROFILE_ISOLATION_READY
|
||||||
|
- AIOS_GROUP_AGENT_ROUTING_READY
|
||||||
|
- AIOS_OWNER_AGENT_READY
|
||||||
|
- AIOS_CONTEXT_AUDIT_READY
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
- Safety policy overrides all user customization.
|
||||||
|
- No cross-user profile access.
|
||||||
|
- No personal profile data in shared Git configuration.
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# CMD-20260709-fix-openapi-workspace-permission
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Fix Git workspace permission issue that blocks writing OpenAPI files.
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
ChatGPT Actions failed to write:
|
||||||
|
|
||||||
|
```text
|
||||||
|
openapi/agent-context-isolation.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Error:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Permission denied: /data/aios-gitops/workspace/openapi/agent-context-isolation.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Execute
|
||||||
|
|
||||||
|
1. Inspect permissions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -ld /data/aios-gitops/workspace/openapi
|
||||||
|
ls -la /data/aios-gitops/workspace/openapi | head
|
||||||
|
stat /data/aios-gitops/workspace/openapi
|
||||||
|
ps aux | grep -E 'aios-gitops|model-api|actions' | grep -v grep
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Identify runtime write user for Actions API.
|
||||||
|
|
||||||
|
3. Fix ownership/permissions with least privilege. Expected owner should match the GitOps runtime write user, likely `aiman` or the service user.
|
||||||
|
|
||||||
|
Example only after confirming user/group:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chown -R <runtime_user>:<runtime_group> /data/aios-gitops/workspace/openapi
|
||||||
|
sudo chmod -R u+rwX,g+rwX /data/aios-gitops/workspace/openapi
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Do not make the directory world-writable unless explicitly approved.
|
||||||
|
|
||||||
|
5. Retry creating:
|
||||||
|
|
||||||
|
```text
|
||||||
|
openapi/agent-context-isolation.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Commit through GitOps patch branch only:
|
||||||
|
|
||||||
|
```text
|
||||||
|
patch/group-owner-agent-context-isolation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- OpenAPI file write succeeds.
|
||||||
|
- Git commit succeeds.
|
||||||
|
- No direct main push.
|
||||||
|
- No secret written.
|
||||||
|
- Permission repair is recorded in audit.
|
||||||
|
|
||||||
|
Expected signal:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AIOS_OPENAPI_WORKSPACE_PERMISSION_READY
|
||||||
|
```
|
||||||
|
|
||||||
|
## Forbidden
|
||||||
|
|
||||||
|
- Do not push main.
|
||||||
|
- Do not create tag.
|
||||||
|
- Do not chmod 777 unless owner explicitly approves.
|
||||||
|
- Do not write secrets.
|
||||||
|
- Do not change unrelated directories.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "branch",
|
||||||
|
"branch": "patch/group-owner-agent-context-isolation",
|
||||||
|
"base": "main",
|
||||||
|
"created_by": "chatgpt-gitops-bot",
|
||||||
|
"created_at": "2026-07-09T01:33:04Z",
|
||||||
|
"direct_main_push": false,
|
||||||
|
"owner_approval_required": true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user