feat(permissions): expose network permission state (#103)
This commit is contained in:
19
.ideai/memory/ticket103-network-permission-ux-surface.md
Normal file
19
.ideai/memory/ticket103-network-permission-ux-surface.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
name: ticket103-network-permission-ux-surface
|
||||||
|
description: memory note ticket103-network-permission-ux-surface
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
---
|
||||||
|
title: "Ticket #103 - UX surface for network permission"
|
||||||
|
type: reference
|
||||||
|
description: "Stable UX convention for exposing network permission in IdeA: Permissions panel as primary editor, Agents as compact summary, Terminal as contextual failure surface, and explicit runtime-locked state."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ticket #103 — UX surface for network permission
|
||||||
|
|
||||||
|
- Keep a single primary edit surface in `Permissions > Système`.
|
||||||
|
- Mirror the effective network state in the Agents list as a compact badge.
|
||||||
|
- Use the Terminal only for contextual, actionable failures.
|
||||||
|
- Distinguish clearly between user policy, effective state, and runtime lock.
|
||||||
|
- If the runtime cannot elevate permission in the active session, the control must be read-only and the UI must say so explicitly.
|
||||||
99
.ideai/tickets/103/carnet.md
Normal file
99
.ideai/tickets/103/carnet.md
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#103"
|
||||||
|
version: 4
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
||||||
|
updatedAt: 1785013507979
|
||||||
|
---
|
||||||
|
# Carnet #103 — permission réseau exposée dans IdeA
|
||||||
|
|
||||||
|
## Problème
|
||||||
|
Certaines commandes lancées par les agents s'exécutent dans un environnement où le réseau est restreint, sans visibilité ni action claire dans IdeA. Exemple live : rebuild AppImage OK jusqu'au bundling, puis `appimagetool` échoue à télécharger le runtime GitHub car la session agent est `network restricted` + `approval policy: never`.
|
||||||
|
|
||||||
|
## Décision UX
|
||||||
|
Mémoire : `ticket103-network-permission-ux-surface`.
|
||||||
|
|
||||||
|
- Surface principale : `Permissions > Système`.
|
||||||
|
- Miroirs de lecture : badge compact dans `Agents`, bannière/erreur contextualisée dans `Terminal`.
|
||||||
|
- États visibles : `Réseau autorisé`, `Réseau interdit`, `Demande d'autorisation`, `Verrouillé par le runtime`.
|
||||||
|
- Distinction obligatoire : politique voulue, état effectif, verrou runtime.
|
||||||
|
- Si le runtime externe ne permet pas l'élévation dans la session active : contrôle read-only + explication explicite.
|
||||||
|
|
||||||
|
## Cadrage architecture
|
||||||
|
Ne pas étendre le modèle existant `ProjectPermissions` fichier/bash : le réseau n'est ni une capability filesystem ni une règle Landlock. Ajouter un modèle/read-model séparé de permissions système.
|
||||||
|
|
||||||
|
### Domaine / DTO V1
|
||||||
|
- `NetworkPolicy = "allow" | "deny" | "ask"`.
|
||||||
|
- `SystemPermissionSet { network?: NetworkPolicy }`.
|
||||||
|
- `ProjectSystemPermissions { version, projectDefault?: SystemPermissionSet, agents?: [{ agentId, permissions: SystemPermissionSet }] }`.
|
||||||
|
- `ResolvedAgentSystemPermissions` :
|
||||||
|
- `wanted: NetworkPolicy | null`
|
||||||
|
- `effective: NetworkPolicy`
|
||||||
|
- `runtimeLock: { state: "none" | "locked", source?: string, reason?: string }`
|
||||||
|
- `control: { mode: "editable" | "readOnly", reason?: string }`
|
||||||
|
|
||||||
|
### Ports / use cases
|
||||||
|
- `SystemPermissionStore`.
|
||||||
|
- `GetProjectSystemPermissions`.
|
||||||
|
- `UpdateProjectSystemPermissions`.
|
||||||
|
- `UpdateAgentSystemPermissions`.
|
||||||
|
- `ResolveAgentSystemPermissions`.
|
||||||
|
- `RuntimePermissionProbe` : expose ce que le runtime hôte/fournisseur autorise réellement et s'il verrouille le réseau.
|
||||||
|
|
||||||
|
### API/commands attendus
|
||||||
|
- `get_project_system_permissions(projectId) -> ProjectSystemPermissionsDto`
|
||||||
|
- `update_project_system_permissions({ projectId, permissions }) -> ProjectSystemPermissionsDto`
|
||||||
|
- `update_agent_system_permissions({ projectId, agentId, permissions }) -> ProjectSystemPermissionsDto`
|
||||||
|
- `resolve_agent_system_permissions({ projectId, agentId }) -> ResolvedAgentSystemPermissionsDto`
|
||||||
|
|
||||||
|
### Limite produit V1
|
||||||
|
Implémentable maintenant : persister la politique voulue, afficher `wanted/effective/runtimeLock`, rendre le contrôle read-only si runtime verrouillé/non inspectable, badges agents, bannière terminal.
|
||||||
|
|
||||||
|
Non promis en V1 : changer effectivement la permission réseau d'une session fournisseur déjà lancée ou élever un runtime externe `network restricted` / `approval never`. IdeA doit l'expliquer plutôt que simuler une élévation.
|
||||||
|
|
||||||
|
## Découpage
|
||||||
|
### DevBackend
|
||||||
|
1. Ajouter domaine `system permissions` séparé de LP1 permissions fichier/bash.
|
||||||
|
2. Ajouter store + use cases + DTO + commands Tauri/HTTP.
|
||||||
|
3. Ajouter `RuntimePermissionProbe` read-only au composition root.
|
||||||
|
4. Ajouter read-model `resolve_agent_system_permissions`.
|
||||||
|
5. Optionnel si simple : mapper des échecs réseau vers un code stable `NETWORK_LOCKED` ou `NETWORK_UNAVAILABLE`.
|
||||||
|
|
||||||
|
### DevFrontend
|
||||||
|
1. Étendre types domaine, ports, adapters Tauri/HTTP/mock.
|
||||||
|
2. Ajouter la sous-section `Réseau` dans `Permissions > Système`.
|
||||||
|
3. Ajouter badge compact dans `Agents`.
|
||||||
|
4. Ajouter bannière/erreur terminal contextualisée pour état verrouillé/échec réseau.
|
||||||
|
|
||||||
|
### QA
|
||||||
|
- Projet sans config : état cohérent, pas de faux `allow`.
|
||||||
|
- Save project default puis override agent : relecture identique.
|
||||||
|
- Resolve distingue `wanted`, `effective`, `runtimeLock`.
|
||||||
|
- Probe `locked` : contrôle read-only + message visible.
|
||||||
|
- Non-régression `Permissions > Système` existant fichier/bash.
|
||||||
|
- Aucun moteur ne reçoit de faux flag réseau au spawn.
|
||||||
|
- Badge agents et bannière terminal reflètent l'état effectif.
|
||||||
|
|
||||||
|
## Validation 2026-07-25
|
||||||
|
QA verte sur le périmètre #103.
|
||||||
|
|
||||||
|
Commandes exécutées par QA :
|
||||||
|
- `cargo test -p domain system_permissions`
|
||||||
|
- `cargo test -p application --test system_permission_usecases`
|
||||||
|
- `cargo test -p infrastructure --test system_permission_store`
|
||||||
|
- `cargo test -p app-tauri --test dto_system_permissions`
|
||||||
|
- `cargo test -p web-server allowlisted`
|
||||||
|
- `npm run typecheck`
|
||||||
|
- `npx vitest run src/features/permissions/permissions.test.tsx src/features/agents/agents.test.tsx src/features/terminals/TerminalView.test.tsx`
|
||||||
|
- `npx vitest run src/features/permissions/permissions.test.tsx`
|
||||||
|
|
||||||
|
Résultats : backend ciblé vert, frontend typecheck vert, tests frontend ciblés 49 passed.
|
||||||
|
|
||||||
|
## État Git
|
||||||
|
Implémentation validée dans le working tree courant, mais commit/merge non réalisé par Main :
|
||||||
|
- l'agent Git headless renvoie un pseudo-appel outil au lieu d'agir ;
|
||||||
|
- Main ne peut pas écrire `.git/index.lock` depuis cette session (`Read-only file system`).
|
||||||
|
|
||||||
|
Le commit local reste à faire manuellement ou via un agent Git fonctionnel.
|
||||||
|
|
||||||
|
## Critère de clôture
|
||||||
|
Tests pertinents verts, commit/merge local par Git, puis rebuild AppImage.
|
||||||
39
.ideai/tickets/103/issue.md
Normal file
39
.ideai/tickets/103/issue.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
id: "3d9021da-26c3-439d-9463-8d206bd06f1b"
|
||||||
|
number: 103
|
||||||
|
title: "Exposer et piloter la permission réseau des agents/commandes dans IdeA"
|
||||||
|
status: "qa"
|
||||||
|
priority: "high"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
||||||
|
createdAt: 1785011668081
|
||||||
|
updatedAt: 1785013507979
|
||||||
|
version: 4
|
||||||
|
---
|
||||||
|
## Problème
|
||||||
|
|
||||||
|
Aujourd'hui certaines commandes lancées par les agents s'exécutent dans un environnement où le réseau est restreint, sans que l'utilisateur puisse le voir ni l'autoriser depuis IdeA. Exemple réel : rebuild AppImage OK jusqu'au bundling, puis `appimagetool` échoue à télécharger le runtime AppImage depuis GitHub (`Failed to download runtime: server returned status code 0`) parce que la session agent a `network restricted` et `approval policy: never`.
|
||||||
|
|
||||||
|
## Besoin utilisateur
|
||||||
|
|
||||||
|
Depuis IdeA, l'utilisateur doit pouvoir comprendre et piloter cette permission réseau :
|
||||||
|
- voir qu'un agent/profil/session est en mode réseau interdit ou autorisé ;
|
||||||
|
- configurer la politique réseau attendue pour les agents/commandes ;
|
||||||
|
- éviter les échecs opaques de commandes qui ont légitimement besoin d'Internet (build, install, téléchargement runtime, docs, dépendances) ;
|
||||||
|
- conserver un comportement sûr par défaut et explicite.
|
||||||
|
|
||||||
|
## Attendu produit
|
||||||
|
|
||||||
|
Définir puis implémenter une surface IdeA pour exposer cette permission. La solution doit respecter le modèle de permissions/sandbox existant et clarifier la limite éventuelle : si le sandbox fournisseur impose `network restricted` sans possibilité d'élévation runtime, IdeA doit l'expliquer plutôt que faire croire que le réseau est activable.
|
||||||
|
|
||||||
|
## Critères d'acceptation
|
||||||
|
|
||||||
|
- Une surface UI ou configuration explicite permet de voir la politique réseau applicable aux agents/commandes.
|
||||||
|
- L'utilisateur dispose d'une action ou d'un réglage clair quand IdeA peut piloter cette permission.
|
||||||
|
- Si la permission est imposée par le runtime externe et non modifiable, l'UI l'indique clairement.
|
||||||
|
- Les agents/commandes ne gagnent pas l'accès réseau silencieusement.
|
||||||
|
- Tests pertinents verts.
|
||||||
|
- AppImage reconstruite après livraison.
|
||||||
@ -14,19 +14,21 @@ use application::{
|
|||||||
CloseProjectInput, CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput,
|
CloseProjectInput, CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput,
|
||||||
DeleteAgentInput, DeleteEmbedderProfileInput, DeleteLayoutInput, DeleteMemoryInput,
|
DeleteAgentInput, DeleteEmbedderProfileInput, DeleteLayoutInput, DeleteMemoryInput,
|
||||||
DeleteSkillInput, DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput,
|
DeleteSkillInput, DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput,
|
||||||
GetProjectWorkStateInput, GitBranchesInput, GitCheckoutInput, GitCommitInput, GitGraphInput,
|
GetProjectSystemPermissionsInput, GetProjectWorkStateInput, GitBranchesInput, GitCheckoutInput,
|
||||||
GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput, InspectConversationInput,
|
GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput,
|
||||||
LaunchAgentInput, ListAgentsInput, ListDevicesInput, ListLayoutsInput, ListMemoriesInput,
|
InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListDevicesInput,
|
||||||
ListResumableAgentsInput, ListSkillsInput, LiveSessions, LoadLayoutInput, McpRuntime,
|
ListLayoutsInput, ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, LiveSessions,
|
||||||
MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadConversationPageInput,
|
LoadLayoutInput, McpRuntime, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput,
|
||||||
ReadMcpToolPermissionsInput, ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput,
|
ReadConversationPageInput, ReadMcpToolPermissionsInput, ReadMemoryIndexInput,
|
||||||
ReconcileLayoutsInput, ReconcileLiveStateInput, RenameDeviceInput, RenameLayoutInput,
|
ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput, ReconcileLiveStateInput,
|
||||||
ResolveAgentPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
|
RenameDeviceInput, RenameLayoutInput, ResolveAgentPermissionsInput,
|
||||||
|
ResolveAgentSystemPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
|
||||||
RotateConversationLogInput, SetActiveLayoutInput, SnapshotRunningAgentsInput,
|
RotateConversationLogInput, SetActiveLayoutInput, SnapshotRunningAgentsInput,
|
||||||
StopLiveAgentInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
|
StopLiveAgentInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
|
||||||
UpdateAgentContextInput, UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
|
UpdateAgentContextInput, UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
|
||||||
UpdateMemoryInput, UpdateProjectContextInput, UpdateProjectMcpToolPermissionsInput,
|
UpdateAgentSystemPermissionsInput, UpdateMemoryInput, UpdateProjectContextInput,
|
||||||
UpdateProjectPermissionsInput, UpdateSkillInput,
|
UpdateProjectMcpToolPermissionsInput, UpdateProjectPermissionsInput,
|
||||||
|
UpdateProjectSystemPermissionsInput, UpdateSkillInput,
|
||||||
};
|
};
|
||||||
use domain::ports::ModelServerRuntime;
|
use domain::ports::ModelServerRuntime;
|
||||||
use domain::ports::PtyHandle;
|
use domain::ports::PtyHandle;
|
||||||
@ -52,19 +54,22 @@ use crate::dto::{
|
|||||||
MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto,
|
MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto,
|
||||||
ModelServerConfigListDto, OpenCodeProviderListDto, OpenTerminalRequestDto,
|
ModelServerConfigListDto, OpenCodeProviderListDto, OpenTerminalRequestDto,
|
||||||
PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
||||||
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectWorkStateDto,
|
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSystemPermissionsDto,
|
||||||
ReadAgentContextResponseDto, ReadConversationPageRequestDto, ReattachChatDto,
|
ProjectWorkStateDto, ReadAgentContextResponseDto, ReadConversationPageRequestDto,
|
||||||
ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
|
ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
|
||||||
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto, ResumableAgentListDto,
|
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto,
|
||||||
SaveEmbedderProfileRequestDto, SaveModelServerRequestDto,
|
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
|
||||||
|
ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveModelServerRequestDto,
|
||||||
SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
|
SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
|
||||||
SetActiveLayoutResultDto, SkillDto, SkillListDto, StopLiveAgentRequestDto,
|
SetActiveLayoutResultDto, SkillDto, SkillListDto, StopLiveAgentRequestDto,
|
||||||
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
|
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
|
||||||
TemplateListDto, TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
|
TemplateListDto, TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
|
||||||
UpdateAgentContextRequestDto, UpdateAgentMcpToolPermissionsRequestDto,
|
UpdateAgentContextRequestDto, UpdateAgentMcpToolPermissionsRequestDto,
|
||||||
UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
|
UpdateAgentPermissionsRequestDto, UpdateAgentSystemPermissionsRequestDto,
|
||||||
|
UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
|
||||||
UpdateProjectMcpToolPermissionsRequestDto, UpdateProjectPermissionsRequestDto,
|
UpdateProjectMcpToolPermissionsRequestDto, UpdateProjectPermissionsRequestDto,
|
||||||
UpdateSkillRequestDto, UpdateTemplateRequestDto, WriteTerminalRequestDto,
|
UpdateProjectSystemPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
|
||||||
|
WriteTerminalRequestDto,
|
||||||
};
|
};
|
||||||
use crate::embedded_server::{
|
use crate::embedded_server::{
|
||||||
EmbeddedServerStatusDto, ServerExposurePreviewDto, ServerExposureSettingsDto,
|
EmbeddedServerStatusDto, ServerExposurePreviewDto, ServerExposureSettingsDto,
|
||||||
@ -549,6 +554,87 @@ pub async fn resolve_agent_permissions(
|
|||||||
.map_err(ErrorDto::from)
|
.map_err(ErrorDto::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `get_project_system_permissions` — read `.ideai/system-permissions.json`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an [`ErrorDto`] on invalid project id or store failure.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_project_system_permissions(
|
||||||
|
project_id: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<ProjectSystemPermissionsDto, ErrorDto> {
|
||||||
|
let project = resolve_project(&project_id, &state).await?;
|
||||||
|
state
|
||||||
|
.get_project_system_permissions
|
||||||
|
.execute(GetProjectSystemPermissionsInput { project })
|
||||||
|
.await
|
||||||
|
.map(|out| ProjectSystemPermissionsDto(out.permissions))
|
||||||
|
.map_err(ErrorDto::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `update_project_system_permissions` — replace project default system permissions.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an [`ErrorDto`] on invalid project id or store failure.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_project_system_permissions(
|
||||||
|
request: UpdateProjectSystemPermissionsRequestDto,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<ProjectSystemPermissionsDto, ErrorDto> {
|
||||||
|
let project = resolve_project(&request.project_id, &state).await?;
|
||||||
|
state
|
||||||
|
.update_project_system_permissions
|
||||||
|
.execute(UpdateProjectSystemPermissionsInput {
|
||||||
|
project,
|
||||||
|
permissions: request.permissions,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|out| ProjectSystemPermissionsDto(out.permissions))
|
||||||
|
.map_err(ErrorDto::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `update_agent_system_permissions` — replace or remove one agent system override.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an [`ErrorDto`] on invalid ids or store failure.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_agent_system_permissions(
|
||||||
|
request: UpdateAgentSystemPermissionsRequestDto,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<ProjectSystemPermissionsDto, ErrorDto> {
|
||||||
|
let project = resolve_project(&request.project_id, &state).await?;
|
||||||
|
let agent_id = parse_agent_id(&request.agent_id)?;
|
||||||
|
state
|
||||||
|
.update_agent_system_permissions
|
||||||
|
.execute(UpdateAgentSystemPermissionsInput {
|
||||||
|
project,
|
||||||
|
agent_id,
|
||||||
|
permissions: request.permissions,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|out| ProjectSystemPermissionsDto(out.permissions))
|
||||||
|
.map_err(ErrorDto::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `resolve_agent_system_permissions` — resolve wanted plus runtime-constrained system permissions.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an [`ErrorDto`] on invalid ids or store/probe failure.
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn resolve_agent_system_permissions(
|
||||||
|
request: ResolveAgentSystemPermissionsRequestDto,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<ResolvedAgentSystemPermissionsDto, ErrorDto> {
|
||||||
|
let project = resolve_project(&request.project_id, &state).await?;
|
||||||
|
let agent_id = parse_agent_id(&request.agent_id)?;
|
||||||
|
state
|
||||||
|
.resolve_agent_system_permissions
|
||||||
|
.execute(ResolveAgentSystemPermissionsInput { project, agent_id })
|
||||||
|
.await
|
||||||
|
.map(|out| ResolvedAgentSystemPermissionsDto(out.permissions))
|
||||||
|
.map_err(ErrorDto::from)
|
||||||
|
}
|
||||||
|
|
||||||
/// `get_mcp_tool_permissions` — read `.ideai/mcp-tool-permissions.json` plus catalogue.
|
/// `get_mcp_tool_permissions` — read `.ideai/mcp-tool-permissions.json` plus catalogue.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
|
|||||||
@ -229,6 +229,10 @@ pub fn run() {
|
|||||||
commands::update_project_permissions,
|
commands::update_project_permissions,
|
||||||
commands::update_agent_permissions,
|
commands::update_agent_permissions,
|
||||||
commands::resolve_agent_permissions,
|
commands::resolve_agent_permissions,
|
||||||
|
commands::get_project_system_permissions,
|
||||||
|
commands::update_project_system_permissions,
|
||||||
|
commands::update_agent_system_permissions,
|
||||||
|
commands::resolve_agent_system_permissions,
|
||||||
commands::get_mcp_tool_permissions,
|
commands::get_mcp_tool_permissions,
|
||||||
commands::update_project_mcp_tool_permissions,
|
commands::update_project_mcp_tool_permissions,
|
||||||
commands::update_agent_mcp_tool_permissions,
|
commands::update_agent_mcp_tool_permissions,
|
||||||
|
|||||||
69
crates/app-tauri/tests/dto_system_permissions.rs
Normal file
69
crates/app-tauri/tests/dto_system_permissions.rs
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
use app_tauri_lib::dto::{
|
||||||
|
ProjectSystemPermissionsDto, ResolvedAgentSystemPermissionsDto,
|
||||||
|
UpdateProjectSystemPermissionsRequestDto,
|
||||||
|
};
|
||||||
|
use domain::{
|
||||||
|
NetworkPolicy, ProjectSystemPermissions, ResolvedAgentSystemPermissions, RuntimeLock,
|
||||||
|
RuntimeLockState, SystemPermissionControl, SystemPermissionControlMode, SystemPermissionSet,
|
||||||
|
};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_system_permissions_dto_serializes_network_policy_contract() {
|
||||||
|
let dto = ProjectSystemPermissionsDto(ProjectSystemPermissions::new(
|
||||||
|
Some(SystemPermissionSet::new(Some(NetworkPolicy::Ask))),
|
||||||
|
vec![],
|
||||||
|
));
|
||||||
|
|
||||||
|
let value = serde_json::to_value(dto).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
value,
|
||||||
|
json!({
|
||||||
|
"version": 1,
|
||||||
|
"projectDefault": {
|
||||||
|
"network": "ask",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_project_system_permissions_request_deserializes_allow_deny_ask() {
|
||||||
|
let dto: UpdateProjectSystemPermissionsRequestDto = serde_json::from_value(json!({
|
||||||
|
"projectId": "project",
|
||||||
|
"permissions": {
|
||||||
|
"network": "allow",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dto.permissions.and_then(|permissions| permissions.network),
|
||||||
|
Some(NetworkPolicy::Allow)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolved_agent_system_permissions_dto_carries_runtime_lock_and_read_only_control() {
|
||||||
|
let dto = ResolvedAgentSystemPermissionsDto(ResolvedAgentSystemPermissions {
|
||||||
|
wanted: Some(NetworkPolicy::Allow),
|
||||||
|
effective: NetworkPolicy::Deny,
|
||||||
|
runtime_lock: RuntimeLock {
|
||||||
|
state: RuntimeLockState::Locked,
|
||||||
|
source: Some("external-runtime".to_owned()),
|
||||||
|
reason: Some("not inspectable".to_owned()),
|
||||||
|
},
|
||||||
|
control: SystemPermissionControl {
|
||||||
|
mode: SystemPermissionControlMode::ReadOnly,
|
||||||
|
reason: Some("not editable".to_owned()),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let value = serde_json::to_value(dto).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(value["wanted"], "allow");
|
||||||
|
assert_eq!(value["effective"], "deny");
|
||||||
|
assert_eq!(value["runtimeLock"]["state"], "locked");
|
||||||
|
assert_eq!(value["control"]["mode"], "readOnly");
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ pub mod project;
|
|||||||
pub mod remote;
|
pub mod remote;
|
||||||
pub mod skill;
|
pub mod skill;
|
||||||
pub mod sprints;
|
pub mod sprints;
|
||||||
|
pub mod system_permissions;
|
||||||
pub mod template;
|
pub mod template;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod ticket_assistant;
|
pub mod ticket_assistant;
|
||||||
@ -170,6 +171,13 @@ pub use sprints::{
|
|||||||
RenameSprintInput, ReorderSprints, ReorderSprintsInput, ReorderSprintsOutput, SprintListEntry,
|
RenameSprintInput, ReorderSprints, ReorderSprintsInput, ReorderSprintsOutput, SprintListEntry,
|
||||||
SprintOutput, UnassignTicketFromSprint, UnassignTicketFromSprintInput,
|
SprintOutput, UnassignTicketFromSprint, UnassignTicketFromSprintInput,
|
||||||
};
|
};
|
||||||
|
pub use system_permissions::{
|
||||||
|
GetProjectSystemPermissions, GetProjectSystemPermissionsInput,
|
||||||
|
GetProjectSystemPermissionsOutput, ResolveAgentSystemPermissions,
|
||||||
|
ResolveAgentSystemPermissionsInput, ResolveAgentSystemPermissionsOutput,
|
||||||
|
UpdateAgentSystemPermissions, UpdateAgentSystemPermissionsInput,
|
||||||
|
UpdateProjectSystemPermissions, UpdateProjectSystemPermissionsInput,
|
||||||
|
};
|
||||||
pub use template::{
|
pub use template::{
|
||||||
AgentDrift, CreateAgentFromTemplate, CreateAgentFromTemplateInput,
|
AgentDrift, CreateAgentFromTemplate, CreateAgentFromTemplateInput,
|
||||||
CreateAgentFromTemplateOutput, CreateTemplate, CreateTemplateInput, CreateTemplateOutput,
|
CreateAgentFromTemplateOutput, CreateTemplate, CreateTemplateInput, CreateTemplateOutput,
|
||||||
|
|||||||
168
crates/application/src/system_permissions.rs
Normal file
168
crates/application/src/system_permissions.rs
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
//! System permission use cases.
|
||||||
|
//!
|
||||||
|
//! These use cases persist the wanted project/agent policies separately from
|
||||||
|
//! filesystem/bash permissions and resolve them through a read-only runtime
|
||||||
|
//! probe.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use domain::ports::{RuntimePermissionProbe, SystemPermissionStore};
|
||||||
|
use domain::{
|
||||||
|
resolve_agent_system_permissions, AgentId, Project, ProjectSystemPermissions,
|
||||||
|
ResolvedAgentSystemPermissions, SystemPermissionSet,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::error::AppError;
|
||||||
|
|
||||||
|
/// Reads the full project system permission document.
|
||||||
|
pub struct GetProjectSystemPermissions {
|
||||||
|
store: Arc<dyn SystemPermissionStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetProjectSystemPermissions {
|
||||||
|
/// Builds the use case.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(store: Arc<dyn SystemPermissionStore>) -> Self {
|
||||||
|
Self { store }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the read.
|
||||||
|
pub async fn execute(
|
||||||
|
&self,
|
||||||
|
input: GetProjectSystemPermissionsInput,
|
||||||
|
) -> Result<GetProjectSystemPermissionsOutput, AppError> {
|
||||||
|
let permissions = self.store.load_system_permissions(&input.project).await?;
|
||||||
|
Ok(GetProjectSystemPermissionsOutput { permissions })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Input for [`GetProjectSystemPermissions`].
|
||||||
|
pub struct GetProjectSystemPermissionsInput {
|
||||||
|
/// Target project.
|
||||||
|
pub project: Project,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output for project system permission reads/mutations.
|
||||||
|
pub struct GetProjectSystemPermissionsOutput {
|
||||||
|
/// Persisted system permission document.
|
||||||
|
pub permissions: ProjectSystemPermissions,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the project default system permissions.
|
||||||
|
pub struct UpdateProjectSystemPermissions {
|
||||||
|
store: Arc<dyn SystemPermissionStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateProjectSystemPermissions {
|
||||||
|
/// Builds the use case.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(store: Arc<dyn SystemPermissionStore>) -> Self {
|
||||||
|
Self { store }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the mutation.
|
||||||
|
pub async fn execute(
|
||||||
|
&self,
|
||||||
|
input: UpdateProjectSystemPermissionsInput,
|
||||||
|
) -> Result<GetProjectSystemPermissionsOutput, AppError> {
|
||||||
|
let mut doc = self.store.load_system_permissions(&input.project).await?;
|
||||||
|
doc.set_project_default(input.permissions);
|
||||||
|
self.store
|
||||||
|
.save_system_permissions(&input.project, &doc)
|
||||||
|
.await?;
|
||||||
|
Ok(GetProjectSystemPermissionsOutput { permissions: doc })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Input for [`UpdateProjectSystemPermissions`].
|
||||||
|
pub struct UpdateProjectSystemPermissionsInput {
|
||||||
|
/// Target project.
|
||||||
|
pub project: Project,
|
||||||
|
/// New project default policy. `None` removes project defaults.
|
||||||
|
pub permissions: Option<SystemPermissionSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces one agent system permission override.
|
||||||
|
pub struct UpdateAgentSystemPermissions {
|
||||||
|
store: Arc<dyn SystemPermissionStore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateAgentSystemPermissions {
|
||||||
|
/// Builds the use case.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(store: Arc<dyn SystemPermissionStore>) -> Self {
|
||||||
|
Self { store }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the mutation.
|
||||||
|
pub async fn execute(
|
||||||
|
&self,
|
||||||
|
input: UpdateAgentSystemPermissionsInput,
|
||||||
|
) -> Result<GetProjectSystemPermissionsOutput, AppError> {
|
||||||
|
let mut doc = self.store.load_system_permissions(&input.project).await?;
|
||||||
|
doc.set_agent_permissions(input.agent_id, input.permissions);
|
||||||
|
self.store
|
||||||
|
.save_system_permissions(&input.project, &doc)
|
||||||
|
.await?;
|
||||||
|
Ok(GetProjectSystemPermissionsOutput { permissions: doc })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Input for [`UpdateAgentSystemPermissions`].
|
||||||
|
pub struct UpdateAgentSystemPermissionsInput {
|
||||||
|
/// Target project.
|
||||||
|
pub project: Project,
|
||||||
|
/// Target agent.
|
||||||
|
pub agent_id: AgentId,
|
||||||
|
/// New agent policy. `None` removes the override.
|
||||||
|
pub permissions: Option<SystemPermissionSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves effective system permissions for one agent.
|
||||||
|
pub struct ResolveAgentSystemPermissions {
|
||||||
|
store: Arc<dyn SystemPermissionStore>,
|
||||||
|
runtime_probe: Arc<dyn RuntimePermissionProbe>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolveAgentSystemPermissions {
|
||||||
|
/// Builds the use case.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
store: Arc<dyn SystemPermissionStore>,
|
||||||
|
runtime_probe: Arc<dyn RuntimePermissionProbe>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
store,
|
||||||
|
runtime_probe,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the resolution.
|
||||||
|
pub async fn execute(
|
||||||
|
&self,
|
||||||
|
input: ResolveAgentSystemPermissionsInput,
|
||||||
|
) -> Result<ResolveAgentSystemPermissionsOutput, AppError> {
|
||||||
|
let doc = self.store.load_system_permissions(&input.project).await?;
|
||||||
|
let runtime = self
|
||||||
|
.runtime_probe
|
||||||
|
.probe_runtime_permissions(&input.project, input.agent_id)
|
||||||
|
.await?;
|
||||||
|
Ok(ResolveAgentSystemPermissionsOutput {
|
||||||
|
permissions: resolve_agent_system_permissions(&doc, input.agent_id, runtime),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Input for [`ResolveAgentSystemPermissions`].
|
||||||
|
pub struct ResolveAgentSystemPermissionsInput {
|
||||||
|
/// Target project.
|
||||||
|
pub project: Project,
|
||||||
|
/// Target agent.
|
||||||
|
pub agent_id: AgentId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output for [`ResolveAgentSystemPermissions`].
|
||||||
|
pub struct ResolveAgentSystemPermissionsOutput {
|
||||||
|
/// Resolved system permissions.
|
||||||
|
pub permissions: ResolvedAgentSystemPermissions,
|
||||||
|
}
|
||||||
154
crates/application/tests/system_permission_usecases.rs
Normal file
154
crates/application/tests/system_permission_usecases.rs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use application::{
|
||||||
|
ResolveAgentSystemPermissions, ResolveAgentSystemPermissionsInput,
|
||||||
|
UpdateAgentSystemPermissions, UpdateAgentSystemPermissionsInput,
|
||||||
|
UpdateProjectSystemPermissions, UpdateProjectSystemPermissionsInput,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use domain::ids::{AgentId, ProjectId};
|
||||||
|
use domain::ports::{RuntimeError, RuntimePermissionProbe, StoreError, SystemPermissionStore};
|
||||||
|
use domain::project::{Project, ProjectPath};
|
||||||
|
use domain::remote::RemoteRef;
|
||||||
|
use domain::{
|
||||||
|
NetworkPolicy, ProjectSystemPermissions, RuntimeLockState, RuntimePermissionSnapshot,
|
||||||
|
SystemPermissionControlMode, SystemPermissionSet,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct FakeSystemPermissionStore {
|
||||||
|
doc: Mutex<ProjectSystemPermissions>,
|
||||||
|
saves: Mutex<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SystemPermissionStore for FakeSystemPermissionStore {
|
||||||
|
async fn load_system_permissions(
|
||||||
|
&self,
|
||||||
|
_project: &Project,
|
||||||
|
) -> Result<ProjectSystemPermissions, StoreError> {
|
||||||
|
Ok(self.doc.lock().unwrap().clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_system_permissions(
|
||||||
|
&self,
|
||||||
|
_project: &Project,
|
||||||
|
permissions: &ProjectSystemPermissions,
|
||||||
|
) -> Result<(), StoreError> {
|
||||||
|
*self.doc.lock().unwrap() = permissions.clone();
|
||||||
|
*self.saves.lock().unwrap() += 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LockedProbe;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RuntimePermissionProbe for LockedProbe {
|
||||||
|
async fn probe_runtime_permissions(
|
||||||
|
&self,
|
||||||
|
_project: &Project,
|
||||||
|
_agent_id: AgentId,
|
||||||
|
) -> Result<RuntimePermissionSnapshot, RuntimeError> {
|
||||||
|
Ok(RuntimePermissionSnapshot::locked_uninspectable())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project() -> Project {
|
||||||
|
Project::new(
|
||||||
|
ProjectId::new_random(),
|
||||||
|
"system-permissions",
|
||||||
|
ProjectPath::new("/home/me/proj").unwrap(),
|
||||||
|
RemoteRef::local(),
|
||||||
|
1_700_000_000_000,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_project_system_permissions_replaces_defaults_and_persists() {
|
||||||
|
let store = Arc::new(FakeSystemPermissionStore::default());
|
||||||
|
let use_case = UpdateProjectSystemPermissions::new(store.clone());
|
||||||
|
|
||||||
|
let out = use_case
|
||||||
|
.execute(UpdateProjectSystemPermissionsInput {
|
||||||
|
project: project(),
|
||||||
|
permissions: Some(SystemPermissionSet::new(Some(NetworkPolicy::Ask))),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
out.permissions.project_default.unwrap().network,
|
||||||
|
Some(NetworkPolicy::Ask)
|
||||||
|
);
|
||||||
|
assert_eq!(*store.saves.lock().unwrap(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn update_agent_system_permissions_adds_and_removes_sparse_override() {
|
||||||
|
let store = Arc::new(FakeSystemPermissionStore::default());
|
||||||
|
let use_case = UpdateAgentSystemPermissions::new(store.clone());
|
||||||
|
let agent = AgentId::new_random();
|
||||||
|
|
||||||
|
use_case
|
||||||
|
.execute(UpdateAgentSystemPermissionsInput {
|
||||||
|
project: project(),
|
||||||
|
agent_id: agent,
|
||||||
|
permissions: Some(SystemPermissionSet::new(Some(NetworkPolicy::Deny))),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.doc
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.agent_permissions(agent)
|
||||||
|
.unwrap()
|
||||||
|
.network,
|
||||||
|
Some(NetworkPolicy::Deny)
|
||||||
|
);
|
||||||
|
|
||||||
|
let out = use_case
|
||||||
|
.execute(UpdateAgentSystemPermissionsInput {
|
||||||
|
project: project(),
|
||||||
|
agent_id: agent,
|
||||||
|
permissions: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(out.permissions.agent_permissions(agent).is_none());
|
||||||
|
assert_eq!(*store.saves.lock().unwrap(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn resolve_agent_system_permissions_exposes_locked_runtime_read_only() {
|
||||||
|
let agent = AgentId::new_random();
|
||||||
|
let store = Arc::new(FakeSystemPermissionStore {
|
||||||
|
doc: Mutex::new(ProjectSystemPermissions::new(
|
||||||
|
Some(SystemPermissionSet::new(Some(NetworkPolicy::Allow))),
|
||||||
|
vec![],
|
||||||
|
)),
|
||||||
|
saves: Mutex::new(0),
|
||||||
|
});
|
||||||
|
let use_case = ResolveAgentSystemPermissions::new(store, Arc::new(LockedProbe));
|
||||||
|
|
||||||
|
let out = use_case
|
||||||
|
.execute(ResolveAgentSystemPermissionsInput {
|
||||||
|
project: project(),
|
||||||
|
agent_id: agent,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(out.permissions.wanted, Some(NetworkPolicy::Allow));
|
||||||
|
assert_eq!(out.permissions.effective, NetworkPolicy::Deny);
|
||||||
|
assert_eq!(out.permissions.runtime_lock.state, RuntimeLockState::Locked);
|
||||||
|
assert_eq!(
|
||||||
|
out.permissions.control.mode,
|
||||||
|
SystemPermissionControlMode::ReadOnly
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -16,7 +16,10 @@ use application::{
|
|||||||
ListProjectsOutput, LiveSessionKind, LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState,
|
ListProjectsOutput, LiveSessionKind, LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState,
|
||||||
StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus, TurnPage, TurnSource, TurnView,
|
StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus, TurnPage, TurnSource, TurnView,
|
||||||
};
|
};
|
||||||
use domain::{AgentBusyState, PageCursor, PageDirection, Project, ProjectId, TurnRole};
|
use domain::{
|
||||||
|
AgentBusyState, PageCursor, PageDirection, Project, ProjectId, ProjectSystemPermissions,
|
||||||
|
ResolvedAgentSystemPermissions, SystemPermissionSet, TurnRole,
|
||||||
|
};
|
||||||
|
|
||||||
pub use crate::ticket_dto::*;
|
pub use crate::ticket_dto::*;
|
||||||
|
|
||||||
@ -1889,6 +1892,16 @@ pub struct ProjectPermissionsDto(pub ProjectPermissions);
|
|||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct EffectivePermissionsDto(pub EffectivePermissions);
|
pub struct EffectivePermissionsDto(pub EffectivePermissions);
|
||||||
|
|
||||||
|
/// Full project system permission document crossing the wire.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct ProjectSystemPermissionsDto(pub ProjectSystemPermissions);
|
||||||
|
|
||||||
|
/// Resolved agent system permissions crossing the wire.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct ResolvedAgentSystemPermissionsDto(pub ResolvedAgentSystemPermissions);
|
||||||
|
|
||||||
/// Canonical MCP tool catalogue classification crossing the wire.
|
/// Canonical MCP tool catalogue classification crossing the wire.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -1959,6 +1972,38 @@ pub struct ResolveAgentPermissionsRequestDto {
|
|||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Request DTO for updating project default system permissions.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateProjectSystemPermissionsRequestDto {
|
||||||
|
/// Id of the owning project.
|
||||||
|
pub project_id: String,
|
||||||
|
/// New project defaults. `null` removes defaults.
|
||||||
|
pub permissions: Option<SystemPermissionSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request DTO for updating one agent system permission override.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateAgentSystemPermissionsRequestDto {
|
||||||
|
/// Id of the owning project.
|
||||||
|
pub project_id: String,
|
||||||
|
/// Target agent id.
|
||||||
|
pub agent_id: String,
|
||||||
|
/// New override. `null` removes the override.
|
||||||
|
pub permissions: Option<SystemPermissionSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request DTO for resolving one agent's effective system permissions.
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ResolveAgentSystemPermissionsRequestDto {
|
||||||
|
/// Id of the owning project.
|
||||||
|
pub project_id: String,
|
||||||
|
/// Target agent id.
|
||||||
|
pub agent_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Request DTO for updating project default MCP tool permissions.
|
/// Request DTO for updating project default MCP tool permissions.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|||||||
@ -22,33 +22,35 @@ use application::{
|
|||||||
DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint, DeleteTemplate,
|
DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint, DeleteTemplate,
|
||||||
DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles, DismissEmbedderSuggestion,
|
DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles, DismissEmbedderSuggestion,
|
||||||
EnsureLocalModelServer, FirstRunState, GetAppExitWorkGuardState, GetLiveStateLean, GetMemory,
|
EnsureLocalModelServer, FirstRunState, GetAppExitWorkGuardState, GetLiveStateLean, GetMemory,
|
||||||
GetProjectPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph,
|
GetProjectPermissions, GetProjectSystemPermissions, GetProjectWorkState, GitBranches,
|
||||||
GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase,
|
GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage,
|
||||||
InspectConversation, InstallPluginFromArchive, InstallPluginFromDirectory,
|
HarvestMemoryFromTurn, HealthUseCase, InspectConversation, InstallPluginFromArchive,
|
||||||
JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents,
|
InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput,
|
||||||
ListAgentsInput, ListDevices, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories,
|
LinkIssues, ListAgents, ListAgentsInput, ListDevices, ListEmbedderProfiles, ListIssues,
|
||||||
ListModelServers, ListOpenCodeProviders, ListPluginRuntimeContributions, ListPlugins,
|
ListLayouts, ListMemories, ListModelServers, ListOpenCodeProviders,
|
||||||
ListProfiles, ListProjects, ListResumableAgents, ListSkills, ListSprints, ListTemplates,
|
ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects, ListResumableAgents,
|
||||||
LiveAgentRegistry, LiveSessions, LiveStateLeanProvider, LiveStateProvider,
|
ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions, LiveStateLeanProvider,
|
||||||
LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue, MoveTabToNewWindow,
|
LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue,
|
||||||
MutateLayout, OnnxModelView, OpenProject, OpenTerminal, OpenTicketAssistant,
|
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
|
||||||
OrchestratorService, PairAttemptLimiter, PairDevice, PermissionProjectorRegistry,
|
OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
|
||||||
ProposeContext, ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue,
|
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
|
||||||
ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory, ReadMemoryIndex, ReadProjectContext,
|
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory,
|
||||||
ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts, ReconcileLiveState,
|
ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
|
||||||
ReconcileLiveStateInput, ReconcilePluginMcpServers, RecordTurn, RecordTurnProvider,
|
ReconcileLiveState, ReconcileLiveStateInput, ReconcilePluginMcpServers, RecordTurn,
|
||||||
ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal,
|
RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint,
|
||||||
ResolveAgentPermissions, ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask,
|
ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveAgentSystemPermissions,
|
||||||
ReviewPluginPackage, RevokeAllDevices, RevokeDevice, RotateConversationLog,
|
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage,
|
||||||
SaveEmbedderProfile, SaveModelServer, SaveOpenCodeProviderProfile, SaveProfile,
|
RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveEmbedderProfile, SaveModelServer,
|
||||||
SessionLimitService, SetActiveLayout, SetPluginEnabled, SnapshotOpenWindows,
|
SaveOpenCodeProviderProfile, SaveProfile, SessionLimitService, SetActiveLayout,
|
||||||
SnapshotRunningAgents, SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode,
|
SetPluginEnabled, SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand,
|
||||||
StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice,
|
StopLiveAgent, StructuredRoutingMode, StructuredSessions, SuggestedThisSession,
|
||||||
UnassignSkillFromAgent, UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues,
|
SyncAgentWithTemplate, TerminalSessions, TouchDevice, UnassignSkillFromAgent,
|
||||||
UpdateAgentContext, UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateIssue,
|
UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues, UpdateAgentContext,
|
||||||
UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
|
UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateAgentSystemPermissions,
|
||||||
UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateSkill, UpdateTemplate,
|
UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
|
||||||
WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateProjectSystemPermissions,
|
||||||
|
UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal,
|
||||||
|
AGENT_MEMORY_RECALL_BUDGET,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
@ -58,9 +60,10 @@ use domain::ports::{
|
|||||||
EmbedderProfileStore, EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator,
|
EmbedderProfileStore, EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator,
|
||||||
IssueNumberAllocator, IssueStore, McpToolPermissionStore, MemoryRecall, MemoryStore,
|
IssueNumberAllocator, IssueStore, McpToolPermissionStore, MemoryRecall, MemoryStore,
|
||||||
PermissionStore, PluginManifestValidator, PluginMcpSupervisor, PluginPackageStore,
|
PermissionStore, PluginManifestValidator, PluginMcpSupervisor, PluginPackageStore,
|
||||||
PluginRegistryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort, ScheduledTask,
|
PluginRegistryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort,
|
||||||
Scheduler, SecretStore, SkillStore, SprintStore, StructuredSessionEnvironmentPreparer,
|
RuntimePermissionProbe, ScheduledTask, Scheduler, SecretStore, SkillStore, SprintStore,
|
||||||
TemplateStore, ToolInvoker, WakeError, WakeReason, WindowStateStore,
|
StructuredSessionEnvironmentPreparer, SystemPermissionStore, TemplateStore, ToolInvoker,
|
||||||
|
WakeError, WakeReason, WindowStateStore,
|
||||||
};
|
};
|
||||||
use domain::profile::{
|
use domain::profile::{
|
||||||
AgentProfile, ContextInjection, McpConfigStrategy, McpTransport, StructuredAdapter,
|
AgentProfile, ContextInjection, McpConfigStrategy, McpTransport, StructuredAdapter,
|
||||||
@ -83,16 +86,16 @@ use infrastructure::{
|
|||||||
FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore, FsMcpToolPermissionStore,
|
FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore, FsMcpToolPermissionStore,
|
||||||
FsMemoryStore, FsModelServerRegistry, FsOrchestratorWatcher, FsPermissionStore,
|
FsMemoryStore, FsModelServerRegistry, FsOrchestratorWatcher, FsPermissionStore,
|
||||||
FsPluginPackageStore, FsPluginRegistryStore, FsProfileStore, FsProjectStore,
|
FsPluginPackageStore, FsPluginRegistryStore, FsProfileStore, FsProjectStore,
|
||||||
FsProviderSessionStore, FsSecretStore, FsSkillStore, FsSprintStore, FsTemplateStore,
|
FsProviderSessionStore, FsSecretStore, FsSkillStore, FsSprintStore, FsSystemPermissionStore,
|
||||||
FsWindowStateStore, Git2Repository, HeuristicHandoffSummarizer, HfModelArtifactDownloader,
|
FsTemplateStore, FsWindowStateStore, Git2Repository, HeuristicHandoffSummarizer,
|
||||||
HttpOpenAiCompatibleProbe, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
|
HfModelArtifactDownloader, HttpOpenAiCompatibleProbe, IdeaiContextStore,
|
||||||
InMemoryPairAttemptLimiter, LlamaCppRuntime, LocalFileSystem, LocalManagedProcess,
|
InMemoryConversationRegistry, InMemoryMailbox, InMemoryPairAttemptLimiter, LlamaCppRuntime,
|
||||||
LocalProcessSpawner, McpServer, MediatedInbox, NaiveMemoryRecall, OrchestratorWatchHandle,
|
LocalFileSystem, LocalManagedProcess, LocalProcessSpawner, McpServer, MediatedInbox,
|
||||||
PortablePtyAdapter, RwFileGuard, StructuredSessionFactory, SystemClock, SystemMillisClock,
|
NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter, ReadOnlyRuntimePermissionProbe,
|
||||||
TemplateToolProvider, TicketAssistantEnvironmentPreparer, TicketToolProvider,
|
RwFileGuard, StructuredSessionFactory, SystemClock, SystemMillisClock, TemplateToolProvider,
|
||||||
TokioBroadcastEventBus, TokioScheduler, ToolPolicyRegistry, UuidGenerator, VectorMemoryRecall,
|
TicketAssistantEnvironmentPreparer, TicketToolProvider, TokioBroadcastEventBus, TokioScheduler,
|
||||||
DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED,
|
ToolPolicyRegistry, UuidGenerator, VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL,
|
||||||
VECTOR_ONNX_ENABLED,
|
ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod dto;
|
pub mod dto;
|
||||||
@ -1063,6 +1066,14 @@ pub struct BackendCore {
|
|||||||
pub update_agent_permissions: Arc<UpdateAgentPermissions>,
|
pub update_agent_permissions: Arc<UpdateAgentPermissions>,
|
||||||
/// Resolve effective permissions for one agent.
|
/// Resolve effective permissions for one agent.
|
||||||
pub resolve_agent_permissions: Arc<ResolveAgentPermissions>,
|
pub resolve_agent_permissions: Arc<ResolveAgentPermissions>,
|
||||||
|
/// Read the project system permission document.
|
||||||
|
pub get_project_system_permissions: Arc<GetProjectSystemPermissions>,
|
||||||
|
/// Update project-level default system permissions.
|
||||||
|
pub update_project_system_permissions: Arc<UpdateProjectSystemPermissions>,
|
||||||
|
/// Update one agent system permission override.
|
||||||
|
pub update_agent_system_permissions: Arc<UpdateAgentSystemPermissions>,
|
||||||
|
/// Resolve effective system permissions for one agent.
|
||||||
|
pub resolve_agent_system_permissions: Arc<ResolveAgentSystemPermissions>,
|
||||||
// --- Windows (L10) ---
|
// --- Windows (L10) ---
|
||||||
/// Detach a tab into a new OS window (persists the workspace topology).
|
/// Detach a tab into a new OS window (persists the workspace topology).
|
||||||
pub move_tab: Arc<MoveTabToNewWindow>,
|
pub move_tab: Arc<MoveTabToNewWindow>,
|
||||||
@ -1640,6 +1651,24 @@ impl BackendCore {
|
|||||||
// --- Project permissions (LP1) ---
|
// --- Project permissions (LP1) ---
|
||||||
let permission_store = Arc::new(FsPermissionStore::new(Arc::clone(&fs_port)));
|
let permission_store = Arc::new(FsPermissionStore::new(Arc::clone(&fs_port)));
|
||||||
let permission_store_port = Arc::clone(&permission_store) as Arc<dyn PermissionStore>;
|
let permission_store_port = Arc::clone(&permission_store) as Arc<dyn PermissionStore>;
|
||||||
|
let system_permission_store = Arc::new(FsSystemPermissionStore::new(Arc::clone(&fs_port)));
|
||||||
|
let system_permission_store_port =
|
||||||
|
Arc::clone(&system_permission_store) as Arc<dyn SystemPermissionStore>;
|
||||||
|
let runtime_permission_probe =
|
||||||
|
Arc::new(ReadOnlyRuntimePermissionProbe) as Arc<dyn RuntimePermissionProbe>;
|
||||||
|
let get_project_system_permissions = Arc::new(GetProjectSystemPermissions::new(
|
||||||
|
Arc::clone(&system_permission_store_port),
|
||||||
|
));
|
||||||
|
let update_project_system_permissions = Arc::new(UpdateProjectSystemPermissions::new(
|
||||||
|
Arc::clone(&system_permission_store_port),
|
||||||
|
));
|
||||||
|
let update_agent_system_permissions = Arc::new(UpdateAgentSystemPermissions::new(
|
||||||
|
Arc::clone(&system_permission_store_port),
|
||||||
|
));
|
||||||
|
let resolve_agent_system_permissions = Arc::new(ResolveAgentSystemPermissions::new(
|
||||||
|
Arc::clone(&system_permission_store_port),
|
||||||
|
Arc::clone(&runtime_permission_probe),
|
||||||
|
));
|
||||||
let mcp_tool_permission_store =
|
let mcp_tool_permission_store =
|
||||||
Arc::new(FsMcpToolPermissionStore::new(Arc::clone(&fs_port)));
|
Arc::new(FsMcpToolPermissionStore::new(Arc::clone(&fs_port)));
|
||||||
let mcp_tool_permission_store_port =
|
let mcp_tool_permission_store_port =
|
||||||
@ -2686,6 +2715,10 @@ impl BackendCore {
|
|||||||
update_project_permissions,
|
update_project_permissions,
|
||||||
update_agent_permissions,
|
update_agent_permissions,
|
||||||
resolve_agent_permissions,
|
resolve_agent_permissions,
|
||||||
|
get_project_system_permissions,
|
||||||
|
update_project_system_permissions,
|
||||||
|
update_agent_system_permissions,
|
||||||
|
resolve_agent_system_permissions,
|
||||||
create_template,
|
create_template,
|
||||||
read_template,
|
read_template,
|
||||||
update_template,
|
update_template,
|
||||||
|
|||||||
@ -64,6 +64,7 @@ pub mod sandbox;
|
|||||||
pub mod session_limit;
|
pub mod session_limit;
|
||||||
pub mod skill;
|
pub mod skill;
|
||||||
pub mod sprint;
|
pub mod sprint;
|
||||||
|
pub mod system_permissions;
|
||||||
pub mod template;
|
pub mod template;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
|
|
||||||
@ -195,6 +196,13 @@ pub use permission::{
|
|||||||
PERMISSIONS_VERSION,
|
PERMISSIONS_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use system_permissions::{
|
||||||
|
resolve_agent_system_permissions, AgentSystemPermissionOverride, NetworkPolicy,
|
||||||
|
ProjectSystemPermissions, ResolvedAgentSystemPermissions, RuntimeLock, RuntimeLockState,
|
||||||
|
RuntimePermissionSnapshot, SystemPermissionControl, SystemPermissionControlMode,
|
||||||
|
SystemPermissionSet, SYSTEM_PERMISSIONS_VERSION,
|
||||||
|
};
|
||||||
|
|
||||||
pub use plugin::{
|
pub use plugin::{
|
||||||
ContentHash, CustomPluginLayout, PluginBundleUrl, PluginCapability, PluginCommandId,
|
ContentHash, CustomPluginLayout, PluginBundleUrl, PluginCapability, PluginCommandId,
|
||||||
PluginContributionSet, PluginDescriptor, PluginError, PluginId, PluginInstallSource,
|
PluginContributionSet, PluginDescriptor, PluginError, PluginId, PluginInstallSource,
|
||||||
@ -228,7 +236,8 @@ pub use ports::{
|
|||||||
PluginManifestError, PluginManifestValidator, PluginMcpError, PluginMcpSupervisor,
|
PluginManifestError, PluginManifestValidator, PluginMcpError, PluginMcpSupervisor,
|
||||||
PluginPackageStore, PluginRegistryError, PluginRegistryStore, PluginStoreError,
|
PluginPackageStore, PluginRegistryError, PluginRegistryStore, PluginStoreError,
|
||||||
PreparedContext, ProcessError, ProcessSpawner, ProfileStore, ProjectStore, PtyError, PtyHandle,
|
PreparedContext, ProcessError, ProcessSpawner, ProfileStore, ProjectStore, PtyError, PtyHandle,
|
||||||
PtyPort, RemoteError, RemoteHost, RemotePath, RuntimeError, ScheduledTask, Scheduler,
|
PtyPort, RemoteError, RemoteHost, RemotePath, RuntimeError, RuntimePermissionProbe,
|
||||||
SpawnSpec, SprintStore, SprintStoreError, StoreError, StructuredSessionEnvironment,
|
ScheduledTask, Scheduler, SpawnSpec, SprintStore, SprintStoreError, StoreError,
|
||||||
StructuredSessionEnvironmentPreparer, TemplateStore, WindowStateStore,
|
StructuredSessionEnvironment, StructuredSessionEnvironmentPreparer, SystemPermissionStore,
|
||||||
|
TemplateStore, WindowStateStore,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -59,6 +59,7 @@ use crate::project::{Project, ProjectPath};
|
|||||||
use crate::remote::RemoteKind;
|
use crate::remote::RemoteKind;
|
||||||
use crate::skill::{Skill, SkillScope};
|
use crate::skill::{Skill, SkillScope};
|
||||||
use crate::sprint::{Sprint, SprintIndexEntry, SprintVersion};
|
use crate::sprint::{Sprint, SprintIndexEntry, SprintVersion};
|
||||||
|
use crate::system_permissions::{ProjectSystemPermissions, RuntimePermissionSnapshot};
|
||||||
use crate::template::AgentTemplate;
|
use crate::template::AgentTemplate;
|
||||||
use crate::terminal::PtySize;
|
use crate::terminal::PtySize;
|
||||||
|
|
||||||
@ -2026,6 +2027,44 @@ pub trait PermissionStore: Send + Sync {
|
|||||||
) -> Result<(), StoreError>;
|
) -> Result<(), StoreError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads/writes a project's `.ideai/system-permissions.json`.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait SystemPermissionStore: Send + Sync {
|
||||||
|
/// Loads the project's system permission document. Missing file returns the
|
||||||
|
/// default empty document.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// [`StoreError`] on I/O or deserialisation failure.
|
||||||
|
async fn load_system_permissions(
|
||||||
|
&self,
|
||||||
|
project: &Project,
|
||||||
|
) -> Result<ProjectSystemPermissions, StoreError>;
|
||||||
|
|
||||||
|
/// Saves the project's system permission document.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// [`StoreError`] on I/O or serialisation failure.
|
||||||
|
async fn save_system_permissions(
|
||||||
|
&self,
|
||||||
|
project: &Project,
|
||||||
|
permissions: &ProjectSystemPermissions,
|
||||||
|
) -> Result<(), StoreError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only probe for host/provider system permission constraints.
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RuntimePermissionProbe: Send + Sync {
|
||||||
|
/// Returns the effective runtime permission state visible to IdeA.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// [`RuntimeError`] when probing itself fails.
|
||||||
|
async fn probe_runtime_permissions(
|
||||||
|
&self,
|
||||||
|
project: &Project,
|
||||||
|
agent_id: AgentId,
|
||||||
|
) -> Result<RuntimePermissionSnapshot, RuntimeError>;
|
||||||
|
}
|
||||||
|
|
||||||
/// Reads/writes a project's `.ideai/mcp-tool-permissions.json`.
|
/// Reads/writes a project's `.ideai/mcp-tool-permissions.json`.
|
||||||
///
|
///
|
||||||
/// This is intentionally distinct from [`PermissionStore`]: it governs IdeA MCP
|
/// This is intentionally distinct from [`PermissionStore`]: it governs IdeA MCP
|
||||||
|
|||||||
324
crates/domain/src/system_permissions.rs
Normal file
324
crates/domain/src/system_permissions.rs
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
//! System-level agent permissions, distinct from filesystem/bash permissions.
|
||||||
|
//!
|
||||||
|
//! This model stores the policy the user wants IdeA to apply. It does not claim
|
||||||
|
//! that an external assistant runtime can be elevated live: resolution combines
|
||||||
|
//! the wanted policy with a read-only runtime probe.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::ids::AgentId;
|
||||||
|
|
||||||
|
/// Current schema version for `.ideai/system-permissions.json`.
|
||||||
|
pub const SYSTEM_PERMISSIONS_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
/// Wanted/effective network policy.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum NetworkPolicy {
|
||||||
|
/// Network access is wanted/observed as allowed.
|
||||||
|
Allow,
|
||||||
|
/// Network access is wanted/observed as denied.
|
||||||
|
Deny,
|
||||||
|
/// Ask before allowing network access when the runtime supports it.
|
||||||
|
Ask,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional system permissions.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SystemPermissionSet {
|
||||||
|
/// Network policy. `None` means no IdeA-level policy has been set.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub network: Option<NetworkPolicy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemPermissionSet {
|
||||||
|
/// Builds a set from an optional network policy.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(network: Option<NetworkPolicy>) -> Self {
|
||||||
|
Self { network }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the set carries no policy.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_empty(&self) -> bool {
|
||||||
|
self.network.is_none()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-agent system permission override.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AgentSystemPermissionOverride {
|
||||||
|
/// Agent id.
|
||||||
|
pub agent_id: AgentId,
|
||||||
|
/// Agent-specific permissions.
|
||||||
|
pub permissions: SystemPermissionSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentSystemPermissionOverride {
|
||||||
|
/// Builds an override.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(agent_id: AgentId, permissions: SystemPermissionSet) -> Self {
|
||||||
|
Self {
|
||||||
|
agent_id,
|
||||||
|
permissions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persisted project system permission document.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProjectSystemPermissions {
|
||||||
|
/// Document format version.
|
||||||
|
pub version: u32,
|
||||||
|
/// Optional project-wide default permissions.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub project_default: Option<SystemPermissionSet>,
|
||||||
|
/// Per-agent overrides.
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub agents: Vec<AgentSystemPermissionOverride>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProjectSystemPermissions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
version: SYSTEM_PERMISSIONS_VERSION,
|
||||||
|
project_default: None,
|
||||||
|
agents: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectSystemPermissions {
|
||||||
|
/// Builds a document.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
project_default: Option<SystemPermissionSet>,
|
||||||
|
agents: Vec<AgentSystemPermissionOverride>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
version: SYSTEM_PERMISSIONS_VERSION,
|
||||||
|
project_default,
|
||||||
|
agents,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the project default permissions.
|
||||||
|
pub fn set_project_default(&mut self, permissions: Option<SystemPermissionSet>) {
|
||||||
|
self.project_default = permissions.filter(|set| !set.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the override for one agent, if present.
|
||||||
|
#[must_use]
|
||||||
|
pub fn agent_permissions(&self, agent_id: AgentId) -> Option<&SystemPermissionSet> {
|
||||||
|
self.agents
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.agent_id == agent_id)
|
||||||
|
.map(|entry| &entry.permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces or removes an agent override.
|
||||||
|
pub fn set_agent_permissions(
|
||||||
|
&mut self,
|
||||||
|
agent_id: AgentId,
|
||||||
|
permissions: Option<SystemPermissionSet>,
|
||||||
|
) {
|
||||||
|
self.agents.retain(|entry| entry.agent_id != agent_id);
|
||||||
|
if let Some(permissions) = permissions.filter(|set| !set.is_empty()) {
|
||||||
|
self.agents
|
||||||
|
.push(AgentSystemPermissionOverride::new(agent_id, permissions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves the wanted network policy before runtime constraints.
|
||||||
|
#[must_use]
|
||||||
|
pub fn wanted_network_for(&self, agent_id: AgentId) -> Option<NetworkPolicy> {
|
||||||
|
self.agent_permissions(agent_id)
|
||||||
|
.and_then(|set| set.network)
|
||||||
|
.or_else(|| self.project_default.as_ref().and_then(|set| set.network))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime lock state.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum RuntimeLockState {
|
||||||
|
/// No runtime lock is known.
|
||||||
|
None,
|
||||||
|
/// Runtime locks the effective network policy.
|
||||||
|
Locked,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime lock details.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct RuntimeLock {
|
||||||
|
/// Lock state.
|
||||||
|
pub state: RuntimeLockState,
|
||||||
|
/// Runtime/source that imposes the lock, when known.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub source: Option<String>,
|
||||||
|
/// Human-readable reason.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimeLock {
|
||||||
|
/// No known runtime lock.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn none() -> Self {
|
||||||
|
Self {
|
||||||
|
state: RuntimeLockState::None,
|
||||||
|
source: None,
|
||||||
|
reason: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runtime-locked state.
|
||||||
|
#[must_use]
|
||||||
|
pub fn locked(source: impl Into<String>, reason: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
state: RuntimeLockState::Locked,
|
||||||
|
source: Some(source.into()),
|
||||||
|
reason: Some(reason.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UI control mode.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum SystemPermissionControlMode {
|
||||||
|
/// IdeA can edit the wanted policy.
|
||||||
|
Editable,
|
||||||
|
/// IdeA can only display the state.
|
||||||
|
ReadOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// UI control state.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SystemPermissionControl {
|
||||||
|
/// Control mode.
|
||||||
|
pub mode: SystemPermissionControlMode,
|
||||||
|
/// Human-readable reason for read-only state.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub reason: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemPermissionControl {
|
||||||
|
/// Editable control.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn editable() -> Self {
|
||||||
|
Self {
|
||||||
|
mode: SystemPermissionControlMode::Editable,
|
||||||
|
reason: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only control.
|
||||||
|
#[must_use]
|
||||||
|
pub fn read_only(reason: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
mode: SystemPermissionControlMode::ReadOnly,
|
||||||
|
reason: Some(reason.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only snapshot from the host/provider runtime.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct RuntimePermissionSnapshot {
|
||||||
|
/// Effective network policy observed or conservatively inferred.
|
||||||
|
pub effective_network: NetworkPolicy,
|
||||||
|
/// Runtime lock details.
|
||||||
|
pub runtime_lock: RuntimeLock,
|
||||||
|
/// Control state exposed to the UI.
|
||||||
|
pub control: SystemPermissionControl,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuntimePermissionSnapshot {
|
||||||
|
/// Snapshot for a runtime that IdeA cannot inspect or elevate.
|
||||||
|
#[must_use]
|
||||||
|
pub fn locked_uninspectable() -> Self {
|
||||||
|
const REASON: &str =
|
||||||
|
"IdeA cannot inspect or change network access for the active external runtime.";
|
||||||
|
Self {
|
||||||
|
effective_network: NetworkPolicy::Deny,
|
||||||
|
runtime_lock: RuntimeLock::locked("external-runtime", REASON),
|
||||||
|
control: SystemPermissionControl::read_only(REASON),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolved read-model for one agent.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ResolvedAgentSystemPermissions {
|
||||||
|
/// Wanted network policy from project/agent configuration.
|
||||||
|
pub wanted: Option<NetworkPolicy>,
|
||||||
|
/// Effective network policy after runtime constraints.
|
||||||
|
pub effective: NetworkPolicy,
|
||||||
|
/// Runtime lock state.
|
||||||
|
pub runtime_lock: RuntimeLock,
|
||||||
|
/// Whether the UI can edit the policy live.
|
||||||
|
pub control: SystemPermissionControl,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves wanted system permissions against the runtime snapshot.
|
||||||
|
#[must_use]
|
||||||
|
pub fn resolve_agent_system_permissions(
|
||||||
|
doc: &ProjectSystemPermissions,
|
||||||
|
agent_id: AgentId,
|
||||||
|
runtime: RuntimePermissionSnapshot,
|
||||||
|
) -> ResolvedAgentSystemPermissions {
|
||||||
|
let wanted = doc.wanted_network_for(agent_id);
|
||||||
|
let effective = match runtime.runtime_lock.state {
|
||||||
|
RuntimeLockState::Locked => runtime.effective_network,
|
||||||
|
RuntimeLockState::None => wanted.unwrap_or(runtime.effective_network),
|
||||||
|
};
|
||||||
|
ResolvedAgentSystemPermissions {
|
||||||
|
wanted,
|
||||||
|
effective,
|
||||||
|
runtime_lock: runtime.runtime_lock,
|
||||||
|
control: runtime.control,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_override_wins_over_project_default_for_wanted_policy() {
|
||||||
|
let agent = AgentId::new_random();
|
||||||
|
let doc = ProjectSystemPermissions::new(
|
||||||
|
Some(SystemPermissionSet::new(Some(NetworkPolicy::Ask))),
|
||||||
|
vec![AgentSystemPermissionOverride::new(
|
||||||
|
agent,
|
||||||
|
SystemPermissionSet::new(Some(NetworkPolicy::Deny)),
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(doc.wanted_network_for(agent), Some(NetworkPolicy::Deny));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn locked_runtime_controls_effective_policy_without_fake_allow() {
|
||||||
|
let agent = AgentId::new_random();
|
||||||
|
let doc = ProjectSystemPermissions::default();
|
||||||
|
|
||||||
|
let resolved = resolve_agent_system_permissions(
|
||||||
|
&doc,
|
||||||
|
agent,
|
||||||
|
RuntimePermissionSnapshot::locked_uninspectable(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(resolved.wanted, None);
|
||||||
|
assert_eq!(resolved.effective, NetworkPolicy::Deny);
|
||||||
|
assert_eq!(resolved.runtime_lock.state, RuntimeLockState::Locked);
|
||||||
|
assert_eq!(resolved.control.mode, SystemPermissionControlMode::ReadOnly);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,6 +36,7 @@ pub mod pty;
|
|||||||
pub mod ratelimit;
|
pub mod ratelimit;
|
||||||
pub mod remote;
|
pub mod remote;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
|
pub mod runtime_permission;
|
||||||
pub mod sandbox;
|
pub mod sandbox;
|
||||||
pub mod scheduler;
|
pub mod scheduler;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
@ -87,6 +88,7 @@ pub use pty::PortablePtyAdapter;
|
|||||||
pub use ratelimit::RateLimitParser;
|
pub use ratelimit::RateLimitParser;
|
||||||
pub use remote::{remote_host, LocalHost};
|
pub use remote::{remote_host, LocalHost};
|
||||||
pub use runtime::CliAgentRuntime;
|
pub use runtime::CliAgentRuntime;
|
||||||
|
pub use runtime_permission::ReadOnlyRuntimePermissionProbe;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub use sandbox::LandlockSandbox;
|
pub use sandbox::LandlockSandbox;
|
||||||
pub use sandbox::{default_enforcer, NoopSandbox};
|
pub use sandbox::{default_enforcer, NoopSandbox};
|
||||||
@ -102,8 +104,8 @@ pub use store::{
|
|||||||
AdaptiveMemoryRecall, BackgroundTaskReconcileReport, EmbedderEnvProbe, FsBackgroundTaskStore,
|
AdaptiveMemoryRecall, BackgroundTaskReconcileReport, EmbedderEnvProbe, FsBackgroundTaskStore,
|
||||||
FsDeviceSessionStore, FsEmbedderProfileStore, FsEmbedderPromptStore, FsLiveStateStore,
|
FsDeviceSessionStore, FsEmbedderProfileStore, FsEmbedderPromptStore, FsLiveStateStore,
|
||||||
FsMcpToolPermissionStore, FsMemoryStore, FsPermissionStore, FsProfileStore, FsProjectStore,
|
FsMcpToolPermissionStore, FsMemoryStore, FsPermissionStore, FsProfileStore, FsProjectStore,
|
||||||
FsSecretStore, FsSkillStore, FsTemplateStore, FsWindowStateStore, HashEmbedder,
|
FsSecretStore, FsSkillStore, FsSystemPermissionStore, FsTemplateStore, FsWindowStateStore,
|
||||||
IdeaiContextStore, NaiveMemoryRecall, OnnxModelInfo, StubEmbedder, VectorMemoryRecall,
|
HashEmbedder, IdeaiContextStore, NaiveMemoryRecall, OnnxModelInfo, StubEmbedder,
|
||||||
DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED,
|
VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS,
|
||||||
VECTOR_ONNX_ENABLED,
|
VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
|
||||||
};
|
};
|
||||||
|
|||||||
24
crates/infrastructure/src/runtime_permission.rs
Normal file
24
crates/infrastructure/src/runtime_permission.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//! Read-only runtime permission probe.
|
||||||
|
//!
|
||||||
|
//! V1 deliberately does not claim live control over provider/network sandboxing.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use domain::ports::{RuntimeError, RuntimePermissionProbe};
|
||||||
|
use domain::{AgentId, Project, RuntimePermissionSnapshot};
|
||||||
|
|
||||||
|
/// Conservative probe used when IdeA cannot inspect or pilot runtime network
|
||||||
|
/// permissions.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ReadOnlyRuntimePermissionProbe;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RuntimePermissionProbe for ReadOnlyRuntimePermissionProbe {
|
||||||
|
async fn probe_runtime_permissions(
|
||||||
|
&self,
|
||||||
|
_project: &Project,
|
||||||
|
_agent_id: AgentId,
|
||||||
|
) -> Result<RuntimePermissionSnapshot, RuntimeError> {
|
||||||
|
Ok(RuntimePermissionSnapshot::locked_uninspectable())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ mod profile;
|
|||||||
mod project;
|
mod project;
|
||||||
mod secrets;
|
mod secrets;
|
||||||
mod skill;
|
mod skill;
|
||||||
|
mod system_permission;
|
||||||
mod template;
|
mod template;
|
||||||
mod vector;
|
mod vector;
|
||||||
mod window_state;
|
mod window_state;
|
||||||
@ -40,6 +41,7 @@ pub use profile::{FsEmbedderProfileStore, FsProfileStore};
|
|||||||
pub use project::FsProjectStore;
|
pub use project::FsProjectStore;
|
||||||
pub use secrets::FsSecretStore;
|
pub use secrets::FsSecretStore;
|
||||||
pub use skill::FsSkillStore;
|
pub use skill::FsSkillStore;
|
||||||
|
pub use system_permission::FsSystemPermissionStore;
|
||||||
pub use template::FsTemplateStore;
|
pub use template::FsTemplateStore;
|
||||||
pub use vector::{should_use_vector, AdaptiveMemoryRecall, VectorMemoryRecall};
|
pub use vector::{should_use_vector, AdaptiveMemoryRecall, VectorMemoryRecall};
|
||||||
pub use window_state::FsWindowStateStore;
|
pub use window_state::FsWindowStateStore;
|
||||||
|
|||||||
68
crates/infrastructure/src/store/system_permission.rs
Normal file
68
crates/infrastructure/src/store/system_permission.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//! Filesystem-backed [`SystemPermissionStore`] for project system permissions.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use domain::ports::{FileSystem, FsError, RemotePath, StoreError, SystemPermissionStore};
|
||||||
|
use domain::{Project, ProjectSystemPermissions};
|
||||||
|
|
||||||
|
const SYSTEM_PERMISSIONS_FILE: &str = "system-permissions.json";
|
||||||
|
|
||||||
|
/// JSON-file implementation for `<project>/.ideai/system-permissions.json`.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FsSystemPermissionStore {
|
||||||
|
fs: Arc<dyn FileSystem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FsSystemPermissionStore {
|
||||||
|
/// Builds the store from an injected filesystem port.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(fs: Arc<dyn FileSystem>) -> Self {
|
||||||
|
Self { fs }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(project: &Project) -> RemotePath {
|
||||||
|
let root = project.root.as_str().trim_end_matches(['/', '\\']);
|
||||||
|
RemotePath::new(format!("{root}/.ideai/{SYSTEM_PERMISSIONS_FILE}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_ideai(&self, project: &Project) -> Result<(), StoreError> {
|
||||||
|
let root = project.root.as_str().trim_end_matches(['/', '\\']);
|
||||||
|
self.fs
|
||||||
|
.create_dir_all(&RemotePath::new(format!("{root}/.ideai")))
|
||||||
|
.await
|
||||||
|
.map_err(|e| StoreError::Io(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SystemPermissionStore for FsSystemPermissionStore {
|
||||||
|
async fn load_system_permissions(
|
||||||
|
&self,
|
||||||
|
project: &Project,
|
||||||
|
) -> Result<ProjectSystemPermissions, StoreError> {
|
||||||
|
match self.fs.read(&Self::path(project)).await {
|
||||||
|
Ok(bytes) => {
|
||||||
|
serde_json::from_slice(&bytes).map_err(|e| StoreError::Serialization(e.to_string()))
|
||||||
|
}
|
||||||
|
Err(FsError::NotFound(_)) => Ok(ProjectSystemPermissions::default()),
|
||||||
|
Err(e) => Err(StoreError::Io(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_system_permissions(
|
||||||
|
&self,
|
||||||
|
project: &Project,
|
||||||
|
permissions: &ProjectSystemPermissions,
|
||||||
|
) -> Result<(), StoreError> {
|
||||||
|
self.ensure_ideai(project).await?;
|
||||||
|
let mut bytes = serde_json::to_vec_pretty(permissions)
|
||||||
|
.map_err(|e| StoreError::Serialization(e.to_string()))?;
|
||||||
|
bytes.push(b'\n');
|
||||||
|
self.fs
|
||||||
|
.write(&Self::path(project), &bytes)
|
||||||
|
.await
|
||||||
|
.map_err(|e| StoreError::Io(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,21 +39,22 @@ use application::{
|
|||||||
CreateAgentInput, CreateMemoryInput, CreateSkillInput, CreateSprintInput, DeleteAgentInput,
|
CreateAgentInput, CreateMemoryInput, CreateSkillInput, CreateSprintInput, DeleteAgentInput,
|
||||||
DeleteEmbedderProfileInput, DeleteIssueInput, DeleteMemoryInput, DeleteSkillInput,
|
DeleteEmbedderProfileInput, DeleteIssueInput, DeleteMemoryInput, DeleteSkillInput,
|
||||||
DeleteSprintInput, DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput,
|
DeleteSprintInput, DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput,
|
||||||
GetProjectWorkStateInput, GitBranchesInput, GitCheckoutInput, GitCommitInput, GitGraphInput,
|
GetProjectSystemPermissionsInput, GetProjectWorkStateInput, GitBranchesInput, GitCheckoutInput,
|
||||||
GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput, InspectConversationInput,
|
GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput,
|
||||||
LaunchAgentInput, LinkIssuesInput, ListAgentsInput, ListDevicesInput, ListIssuesInput,
|
InspectConversationInput, LaunchAgentInput, LinkIssuesInput, ListAgentsInput, ListDevicesInput,
|
||||||
ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, ListSprintsInput, LiveSessions,
|
ListIssuesInput, ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput,
|
||||||
McpRuntime, OpenProjectInput, PairAttemptDecision, PairDeviceInput, RateLimitKey,
|
ListSprintsInput, LiveSessions, McpRuntime, OpenProjectInput, PairAttemptDecision,
|
||||||
ReadAgentContextInput, ReadConversationPageInput, ReadIssueCarnetInput, ReadIssueInput,
|
PairDeviceInput, RateLimitKey, ReadAgentContextInput, ReadConversationPageInput,
|
||||||
ReadMcpToolPermissionsInput, ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput,
|
ReadIssueCarnetInput, ReadIssueInput, ReadMcpToolPermissionsInput, ReadMemoryIndexInput,
|
||||||
RenameDeviceInput, RenameSprintInput, ReorderSprintsInput, ResizeTerminalInput,
|
ReadProjectContextInput, RecallMemoryInput, RenameDeviceInput, RenameSprintInput,
|
||||||
ResolveAgentPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
|
ReorderSprintsInput, ResizeTerminalInput, ResolveAgentPermissionsInput,
|
||||||
|
ResolveAgentSystemPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
|
||||||
RotateConversationLogInput, StopLiveAgentInput, SyncAgentWithTemplateInput, TouchDeviceInput,
|
RotateConversationLogInput, StopLiveAgentInput, SyncAgentWithTemplateInput, TouchDeviceInput,
|
||||||
UnassignSkillFromAgentInput, UnassignTicketFromSprintInput, UnlinkIssuesInput,
|
UnassignSkillFromAgentInput, UnassignTicketFromSprintInput, UnlinkIssuesInput,
|
||||||
UpdateAgentContextInput, UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
|
UpdateAgentContextInput, UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
|
||||||
UpdateIssueCarnetInput, UpdateMemoryInput, UpdateProjectContextInput,
|
UpdateAgentSystemPermissionsInput, UpdateIssueCarnetInput, UpdateMemoryInput,
|
||||||
UpdateProjectMcpToolPermissionsInput, UpdateProjectPermissionsInput, UpdateSkillInput,
|
UpdateProjectContextInput, UpdateProjectMcpToolPermissionsInput, UpdateProjectPermissionsInput,
|
||||||
WriteToTerminalInput,
|
UpdateProjectSystemPermissionsInput, UpdateSkillInput, WriteToTerminalInput,
|
||||||
};
|
};
|
||||||
use domain::ports::PtyHandle;
|
use domain::ports::PtyHandle;
|
||||||
use domain::IssueActor;
|
use domain::IssueActor;
|
||||||
@ -78,22 +79,23 @@ use backend::dto::{
|
|||||||
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
|
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
|
||||||
LaunchAgentRequestDto, LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto,
|
LaunchAgentRequestDto, LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto,
|
||||||
MemoryListDto, OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
MemoryListDto, OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
||||||
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectWorkStateDto,
|
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSystemPermissionsDto,
|
||||||
ReadAgentContextResponseDto, ReadConversationPageRequestDto, RecallMemoryRequestDto,
|
ProjectWorkStateDto, ReadAgentContextResponseDto, ReadConversationPageRequestDto,
|
||||||
ResolveAgentPermissionsRequestDto, ResumableAgentListDto, SaveEmbedderProfileRequestDto,
|
RecallMemoryRequestDto, ResolveAgentPermissionsRequestDto,
|
||||||
SaveProfileRequestDto, SkillDto, SkillListDto, SprintCreateRequestDto, SprintDeleteRequestDto,
|
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
|
||||||
SprintDto, SprintListDto, SprintListRequestDto, SprintRenameRequestDto,
|
ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveProfileRequestDto, SkillDto,
|
||||||
SprintReorderRequestDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
|
SkillListDto, SprintCreateRequestDto, SprintDeleteRequestDto, SprintDto, SprintListDto,
|
||||||
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
|
SprintListRequestDto, SprintRenameRequestDto, SprintReorderRequestDto, StopLiveAgentRequestDto,
|
||||||
TerminalSessionDto, TicketAssignRequestDto, TicketCarnetDto, TicketCreateRequestDto,
|
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
|
||||||
TicketDeleteRequestDto, TicketDto, TicketLinkCommandRequestDto, TicketListPageInput,
|
TemplateListDto, TerminalSessionDto, TicketAssignRequestDto, TicketCarnetDto,
|
||||||
TicketListRequestDto, TicketReadRequestDto, TicketSprintAssignRequestDto,
|
TicketCreateRequestDto, TicketDeleteRequestDto, TicketDto, TicketLinkCommandRequestDto,
|
||||||
|
TicketListPageInput, TicketListRequestDto, TicketReadRequestDto, TicketSprintAssignRequestDto,
|
||||||
TicketSprintUnassignRequestDto, TicketUnlinkCommandRequestDto, TicketUpdateCarnetRequestDto,
|
TicketSprintUnassignRequestDto, TicketUnlinkCommandRequestDto, TicketUpdateCarnetRequestDto,
|
||||||
TicketUpdateRequestDto, TurnPageDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
|
TicketUpdateRequestDto, TurnPageDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
|
||||||
UpdateAgentMcpToolPermissionsRequestDto, UpdateAgentPermissionsRequestDto,
|
UpdateAgentMcpToolPermissionsRequestDto, UpdateAgentPermissionsRequestDto,
|
||||||
UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
|
UpdateAgentSystemPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
|
||||||
UpdateProjectMcpToolPermissionsRequestDto, UpdateProjectPermissionsRequestDto,
|
UpdateProjectMcpToolPermissionsRequestDto, UpdateProjectPermissionsRequestDto,
|
||||||
UpdateSkillRequestDto, UpdateTemplateRequestDto,
|
UpdateProjectSystemPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
|
||||||
};
|
};
|
||||||
use backend::events::DomainEventDto;
|
use backend::events::DomainEventDto;
|
||||||
type PtyChunk = Vec<u8>;
|
type PtyChunk = Vec<u8>;
|
||||||
@ -2396,6 +2398,18 @@ async fn invoke(
|
|||||||
"resolve_agent_permissions" => {
|
"resolve_agent_permissions" => {
|
||||||
invoke_resolve_agent_permissions(&request.args, &state.app).await
|
invoke_resolve_agent_permissions(&request.args, &state.app).await
|
||||||
}
|
}
|
||||||
|
"get_project_system_permissions" => {
|
||||||
|
invoke_get_project_system_permissions(&request.args, &state.app).await
|
||||||
|
}
|
||||||
|
"update_project_system_permissions" => {
|
||||||
|
invoke_update_project_system_permissions(&request.args, &state.app).await
|
||||||
|
}
|
||||||
|
"update_agent_system_permissions" => {
|
||||||
|
invoke_update_agent_system_permissions(&request.args, &state.app).await
|
||||||
|
}
|
||||||
|
"resolve_agent_system_permissions" => {
|
||||||
|
invoke_resolve_agent_system_permissions(&request.args, &state.app).await
|
||||||
|
}
|
||||||
"get_mcp_tool_permissions" => {
|
"get_mcp_tool_permissions" => {
|
||||||
invoke_get_mcp_tool_permissions(&request.args, &state.app).await
|
invoke_get_mcp_tool_permissions(&request.args, &state.app).await
|
||||||
}
|
}
|
||||||
@ -3472,6 +3486,88 @@ async fn invoke_resolve_agent_permissions(
|
|||||||
serde_json::to_value(output).map_err(serialization_error)
|
serde_json::to_value(output).map_err(serialization_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn invoke_get_project_system_permissions(
|
||||||
|
args: &Value,
|
||||||
|
state: &BackendCore,
|
||||||
|
) -> Result<Value, ErrorDto> {
|
||||||
|
let project = resolve_project_readonly(
|
||||||
|
string_arg(args, "projectId", "get_project_system_permissions")?,
|
||||||
|
state,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let output = state
|
||||||
|
.get_project_system_permissions
|
||||||
|
.execute(GetProjectSystemPermissionsInput { project })
|
||||||
|
.await
|
||||||
|
.map(|out| ProjectSystemPermissionsDto(out.permissions))
|
||||||
|
.map_err(ErrorDto::from)?;
|
||||||
|
serde_json::to_value(output).map_err(serialization_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn invoke_update_project_system_permissions(
|
||||||
|
args: &Value,
|
||||||
|
state: &BackendCore,
|
||||||
|
) -> Result<Value, ErrorDto> {
|
||||||
|
let request = required_request::<UpdateProjectSystemPermissionsRequestDto>(
|
||||||
|
"update_project_system_permissions",
|
||||||
|
args,
|
||||||
|
)?;
|
||||||
|
let project = resolve_project_readonly(&request.project_id, state).await?;
|
||||||
|
let output = state
|
||||||
|
.update_project_system_permissions
|
||||||
|
.execute(UpdateProjectSystemPermissionsInput {
|
||||||
|
project,
|
||||||
|
permissions: request.permissions,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|out| ProjectSystemPermissionsDto(out.permissions))
|
||||||
|
.map_err(ErrorDto::from)?;
|
||||||
|
serde_json::to_value(output).map_err(serialization_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn invoke_update_agent_system_permissions(
|
||||||
|
args: &Value,
|
||||||
|
state: &BackendCore,
|
||||||
|
) -> Result<Value, ErrorDto> {
|
||||||
|
let request = required_request::<UpdateAgentSystemPermissionsRequestDto>(
|
||||||
|
"update_agent_system_permissions",
|
||||||
|
args,
|
||||||
|
)?;
|
||||||
|
let project = resolve_project_readonly(&request.project_id, state).await?;
|
||||||
|
let output = state
|
||||||
|
.update_agent_system_permissions
|
||||||
|
.execute(UpdateAgentSystemPermissionsInput {
|
||||||
|
project,
|
||||||
|
agent_id: parse_agent_id(&request.agent_id)?,
|
||||||
|
permissions: request.permissions,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|out| ProjectSystemPermissionsDto(out.permissions))
|
||||||
|
.map_err(ErrorDto::from)?;
|
||||||
|
serde_json::to_value(output).map_err(serialization_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn invoke_resolve_agent_system_permissions(
|
||||||
|
args: &Value,
|
||||||
|
state: &BackendCore,
|
||||||
|
) -> Result<Value, ErrorDto> {
|
||||||
|
let request = required_request::<ResolveAgentSystemPermissionsRequestDto>(
|
||||||
|
"resolve_agent_system_permissions",
|
||||||
|
args,
|
||||||
|
)?;
|
||||||
|
let project = resolve_project_readonly(&request.project_id, state).await?;
|
||||||
|
let output = state
|
||||||
|
.resolve_agent_system_permissions
|
||||||
|
.execute(ResolveAgentSystemPermissionsInput {
|
||||||
|
project,
|
||||||
|
agent_id: parse_agent_id(&request.agent_id)?,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map(|out| ResolvedAgentSystemPermissionsDto(out.permissions))
|
||||||
|
.map_err(ErrorDto::from)?;
|
||||||
|
serde_json::to_value(output).map_err(serialization_error)
|
||||||
|
}
|
||||||
|
|
||||||
async fn invoke_get_mcp_tool_permissions(
|
async fn invoke_get_mcp_tool_permissions(
|
||||||
args: &Value,
|
args: &Value,
|
||||||
state: &BackendCore,
|
state: &BackendCore,
|
||||||
@ -7617,6 +7713,10 @@ mod tests {
|
|||||||
"update_project_permissions",
|
"update_project_permissions",
|
||||||
"update_agent_permissions",
|
"update_agent_permissions",
|
||||||
"resolve_agent_permissions",
|
"resolve_agent_permissions",
|
||||||
|
"get_project_system_permissions",
|
||||||
|
"update_project_system_permissions",
|
||||||
|
"update_agent_system_permissions",
|
||||||
|
"resolve_agent_system_permissions",
|
||||||
"get_mcp_tool_permissions",
|
"get_mcp_tool_permissions",
|
||||||
"update_project_mcp_tool_permissions",
|
"update_project_mcp_tool_permissions",
|
||||||
"update_agent_mcp_tool_permissions",
|
"update_agent_mcp_tool_permissions",
|
||||||
|
|||||||
@ -40,7 +40,10 @@ import type {
|
|||||||
ProjectMcpToolPermissions,
|
ProjectMcpToolPermissions,
|
||||||
ProjectPermissions,
|
ProjectPermissions,
|
||||||
ProjectWorkState,
|
ProjectWorkState,
|
||||||
|
ProjectSystemPermissions,
|
||||||
ProfileAvailability,
|
ProfileAvailability,
|
||||||
|
ResolvedAgentSystemPermissions,
|
||||||
|
SystemPermissionSet,
|
||||||
Skill,
|
Skill,
|
||||||
SkillScope,
|
SkillScope,
|
||||||
Template,
|
Template,
|
||||||
@ -364,6 +367,37 @@ export class HttpPermissionGateway implements PermissionGateway {
|
|||||||
request: { projectId, agentId },
|
request: { projectId, agentId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
getProjectSystemPermissions(projectId: string): Promise<ProjectSystemPermissions> {
|
||||||
|
return this.http.invoke<ProjectSystemPermissions>("get_project_system_permissions", {
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateProjectSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
): Promise<ProjectSystemPermissions> {
|
||||||
|
return this.http.invoke<ProjectSystemPermissions>("update_project_system_permissions", {
|
||||||
|
request: { projectId, permissions },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateAgentSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
): Promise<ProjectSystemPermissions> {
|
||||||
|
return this.http.invoke<ProjectSystemPermissions>("update_agent_system_permissions", {
|
||||||
|
request: { projectId, agentId, permissions },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
resolveAgentSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
): Promise<ResolvedAgentSystemPermissions> {
|
||||||
|
return this.http.invoke<ResolvedAgentSystemPermissions>(
|
||||||
|
"resolve_agent_system_permissions",
|
||||||
|
{ request: { projectId, agentId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
getMcpToolPermissions(projectId: string): Promise<ProjectMcpToolPermissions> {
|
getMcpToolPermissions(projectId: string): Promise<ProjectMcpToolPermissions> {
|
||||||
return this.http.invoke<ProjectMcpToolPermissions>("get_mcp_tool_permissions", { projectId });
|
return this.http.invoke<ProjectMcpToolPermissions>("get_mcp_tool_permissions", { projectId });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,8 +50,10 @@ import type {
|
|||||||
ProjectMcpToolPermissions,
|
ProjectMcpToolPermissions,
|
||||||
ProjectPermissions,
|
ProjectPermissions,
|
||||||
ProjectWorkState,
|
ProjectWorkState,
|
||||||
|
ProjectSystemPermissions,
|
||||||
ProfileAvailability,
|
ProfileAvailability,
|
||||||
ResumableAgent,
|
ResumableAgent,
|
||||||
|
ResolvedAgentSystemPermissions,
|
||||||
ServerExposurePreview,
|
ServerExposurePreview,
|
||||||
ServerExposureSettings,
|
ServerExposureSettings,
|
||||||
Skill,
|
Skill,
|
||||||
@ -59,6 +61,7 @@ import type {
|
|||||||
SkillScope,
|
SkillScope,
|
||||||
Sprint,
|
Sprint,
|
||||||
Template,
|
Template,
|
||||||
|
SystemPermissionSet,
|
||||||
TerminalSession,
|
TerminalSession,
|
||||||
Ticket,
|
Ticket,
|
||||||
TicketCarnet,
|
TicketCarnet,
|
||||||
@ -2186,6 +2189,8 @@ export class MockDeviceGateway implements DeviceGateway {
|
|||||||
/** In-memory permissions gateway. */
|
/** In-memory permissions gateway. */
|
||||||
export class MockPermissionGateway implements PermissionGateway {
|
export class MockPermissionGateway implements PermissionGateway {
|
||||||
private docs = new Map<string, ProjectPermissions>();
|
private docs = new Map<string, ProjectPermissions>();
|
||||||
|
private systemDocs = new Map<string, ProjectSystemPermissions>();
|
||||||
|
private systemRuntime = new Map<string, ResolvedAgentSystemPermissions>();
|
||||||
|
|
||||||
private doc(projectId: string): ProjectPermissions {
|
private doc(projectId: string): ProjectPermissions {
|
||||||
if (!this.docs.has(projectId)) {
|
if (!this.docs.has(projectId)) {
|
||||||
@ -2234,6 +2239,72 @@ export class MockPermissionGateway implements PermissionGateway {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private systemDoc(projectId: string): ProjectSystemPermissions {
|
||||||
|
if (!this.systemDocs.has(projectId)) {
|
||||||
|
this.systemDocs.set(projectId, { version: 1, agents: [] });
|
||||||
|
}
|
||||||
|
return this.systemDocs.get(projectId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getProjectSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
): Promise<ProjectSystemPermissions> {
|
||||||
|
return structuredClone(this.systemDoc(projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProjectSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
): Promise<ProjectSystemPermissions> {
|
||||||
|
const doc = this.systemDoc(projectId);
|
||||||
|
if (permissions && Object.keys(permissions).length > 0) {
|
||||||
|
doc.projectDefault = structuredClone(permissions);
|
||||||
|
} else {
|
||||||
|
delete doc.projectDefault;
|
||||||
|
}
|
||||||
|
return structuredClone(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAgentSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
): Promise<ProjectSystemPermissions> {
|
||||||
|
const doc = this.systemDoc(projectId);
|
||||||
|
const agents = (doc.agents ?? []).filter((entry) => entry.agentId !== agentId);
|
||||||
|
if (permissions && Object.keys(permissions).length > 0) {
|
||||||
|
agents.push({ agentId, permissions: structuredClone(permissions) });
|
||||||
|
}
|
||||||
|
doc.agents = agents;
|
||||||
|
return structuredClone(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveAgentSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
): Promise<ResolvedAgentSystemPermissions> {
|
||||||
|
const runtime = this.systemRuntime.get(agentId);
|
||||||
|
if (runtime) return structuredClone(runtime);
|
||||||
|
const doc = this.systemDoc(projectId);
|
||||||
|
const wanted =
|
||||||
|
doc.agents?.find((entry) => entry.agentId === agentId)?.permissions.network ??
|
||||||
|
doc.projectDefault?.network ??
|
||||||
|
null;
|
||||||
|
return {
|
||||||
|
wanted,
|
||||||
|
effective: wanted ?? "ask",
|
||||||
|
runtimeLock: { state: "none" },
|
||||||
|
control: { mode: "editable" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setResolvedAgentSystemPermissions(
|
||||||
|
agentId: string,
|
||||||
|
resolved: ResolvedAgentSystemPermissions,
|
||||||
|
): void {
|
||||||
|
this.systemRuntime.set(agentId, structuredClone(resolved));
|
||||||
|
}
|
||||||
|
|
||||||
// ── MCP tool permissions (ticket #82) — mirrors the backend catalogue in
|
// ── MCP tool permissions (ticket #82) — mirrors the backend catalogue in
|
||||||
// `crates/infrastructure/src/orchestrator/mcp/tools.rs`, a separate durable
|
// `crates/infrastructure/src/orchestrator/mcp/tools.rs`, a separate durable
|
||||||
// document from the file/command permissions above. ─────────────────────
|
// document from the file/command permissions above. ─────────────────────
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import type {
|
|||||||
PermissionSet,
|
PermissionSet,
|
||||||
ProjectMcpToolPermissions,
|
ProjectMcpToolPermissions,
|
||||||
ProjectPermissions,
|
ProjectPermissions,
|
||||||
|
ProjectSystemPermissions,
|
||||||
|
ResolvedAgentSystemPermissions,
|
||||||
|
SystemPermissionSet,
|
||||||
} from "@/domain";
|
} from "@/domain";
|
||||||
import type { PermissionGateway } from "@/ports";
|
import type { PermissionGateway } from "@/ports";
|
||||||
|
|
||||||
@ -43,6 +46,41 @@ export class TauriPermissionGateway implements PermissionGateway {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProjectSystemPermissions(projectId: string): Promise<ProjectSystemPermissions> {
|
||||||
|
return invoke<ProjectSystemPermissions>("get_project_system_permissions", {
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProjectSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
): Promise<ProjectSystemPermissions> {
|
||||||
|
return invoke<ProjectSystemPermissions>("update_project_system_permissions", {
|
||||||
|
request: { projectId, permissions },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAgentSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
): Promise<ProjectSystemPermissions> {
|
||||||
|
return invoke<ProjectSystemPermissions>("update_agent_system_permissions", {
|
||||||
|
request: { projectId, agentId, permissions },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveAgentSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
): Promise<ResolvedAgentSystemPermissions> {
|
||||||
|
return invoke<ResolvedAgentSystemPermissions>(
|
||||||
|
"resolve_agent_system_permissions",
|
||||||
|
{ request: { projectId, agentId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getMcpToolPermissions(projectId: string): Promise<ProjectMcpToolPermissions> {
|
getMcpToolPermissions(projectId: string): Promise<ProjectMcpToolPermissions> {
|
||||||
return invoke<ProjectMcpToolPermissions>("get_mcp_tool_permissions", {
|
return invoke<ProjectMcpToolPermissions>("get_mcp_tool_permissions", {
|
||||||
projectId,
|
projectId,
|
||||||
|
|||||||
@ -748,6 +748,48 @@ export interface EffectivePermissions {
|
|||||||
fallback: PermissionPosture;
|
fallback: PermissionPosture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Wanted/effective network policy for system permissions. */
|
||||||
|
export type NetworkPolicy = "allow" | "deny" | "ask";
|
||||||
|
|
||||||
|
/** Optional system permission bundle, distinct from file/bash permissions. */
|
||||||
|
export interface SystemPermissionSet {
|
||||||
|
network?: NetworkPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One sparse agent system-permission override. */
|
||||||
|
export interface AgentSystemPermissionOverride {
|
||||||
|
agentId: string;
|
||||||
|
permissions: SystemPermissionSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Full project system-permission document. */
|
||||||
|
export interface ProjectSystemPermissions {
|
||||||
|
version: number;
|
||||||
|
projectDefault?: SystemPermissionSet;
|
||||||
|
agents?: AgentSystemPermissionOverride[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runtime lock details for resolved system permissions. */
|
||||||
|
export interface RuntimeLock {
|
||||||
|
state: "none" | "locked";
|
||||||
|
source?: string;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the UI can edit the wanted system policy. */
|
||||||
|
export interface SystemPermissionControl {
|
||||||
|
mode: "editable" | "readOnly";
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolved wanted/effective system permissions for one agent. */
|
||||||
|
export interface ResolvedAgentSystemPermissions {
|
||||||
|
wanted: NetworkPolicy | null;
|
||||||
|
effective: NetworkPolicy;
|
||||||
|
runtimeLock: RuntimeLock;
|
||||||
|
control: SystemPermissionControl;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// MCP tool permissions (ticket #82) — distinct from the file/command
|
// MCP tool permissions (ticket #82) — distinct from the file/command
|
||||||
// permissions above: an allowlist of exact MCP tool names, applied by the MCP
|
// permissions above: an allowlist of exact MCP tool names, applied by the MCP
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
* the existing `createAgent` path with the selected profile.
|
* the existing `createAgent` path with the selected profile.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Button, Input, Panel, Spinner, cn } from "@/shared";
|
import { Button, Input, Panel, Spinner, cn } from "@/shared";
|
||||||
import { TerminalView } from "@/features/terminals/TerminalView";
|
import { TerminalView } from "@/features/terminals/TerminalView";
|
||||||
@ -25,6 +25,7 @@ import { useAgents } from "./useAgents";
|
|||||||
import { correlateModelServerStatus } from "./modelServerLaunch";
|
import { correlateModelServerStatus } from "./modelServerLaunch";
|
||||||
import { AgentLimitBadge } from "./AgentLimitBadge";
|
import { AgentLimitBadge } from "./AgentLimitBadge";
|
||||||
import { ModelServerLaunchBadge } from "./ModelServerLaunchBadge";
|
import { ModelServerLaunchBadge } from "./ModelServerLaunchBadge";
|
||||||
|
import type { ResolvedAgentSystemPermissions } from "@/domain";
|
||||||
|
|
||||||
export interface AgentsPanelProps {
|
export interface AgentsPanelProps {
|
||||||
/** The project whose agents to manage. */
|
/** The project whose agents to manage. */
|
||||||
@ -41,6 +42,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
|||||||
const drift = useDrift(projectId);
|
const drift = useDrift(projectId);
|
||||||
const gateways = useGateways();
|
const gateways = useGateways();
|
||||||
const templateGw = gateways.template ?? null;
|
const templateGw = gateways.template ?? null;
|
||||||
|
const permissionGw = gateways.permission ?? null;
|
||||||
|
|
||||||
// Create form state
|
// Create form state
|
||||||
const [newName, setNewName] = useState("");
|
const [newName, setNewName] = useState("");
|
||||||
@ -130,6 +132,45 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
|||||||
const [agentSessions, setAgentSessions] = useState<Record<string, string>>(
|
const [agentSessions, setAgentSessions] = useState<Record<string, string>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
const [networkByAgent, setNetworkByAgent] = useState<
|
||||||
|
Record<string, ResolvedAgentSystemPermissions>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!permissionGw || vm.agents.length === 0) {
|
||||||
|
setNetworkByAgent({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cancelled = false;
|
||||||
|
void Promise.all(
|
||||||
|
vm.agents.map(async (candidate) => {
|
||||||
|
try {
|
||||||
|
return [
|
||||||
|
candidate.id,
|
||||||
|
await permissionGw.resolveAgentSystemPermissions(
|
||||||
|
projectId,
|
||||||
|
candidate.id,
|
||||||
|
),
|
||||||
|
] as const;
|
||||||
|
} catch {
|
||||||
|
return [candidate.id, null] as const;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).then((pairs) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setNetworkByAgent(
|
||||||
|
Object.fromEntries(
|
||||||
|
pairs.filter(
|
||||||
|
(pair): pair is readonly [string, ResolvedAgentSystemPermissions] =>
|
||||||
|
pair[1] !== null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [permissionGw, projectId, vm.agents]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pending profile change awaiting confirmation: the target agent + the chosen
|
* Pending profile change awaiting confirmation: the target agent + the chosen
|
||||||
@ -342,6 +383,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
|||||||
vm.modelServerStatusByServer,
|
vm.modelServerStatusByServer,
|
||||||
);
|
);
|
||||||
const launchFailure = vm.launchFailureByAgent[a.id];
|
const launchFailure = vm.launchFailureByAgent[a.id];
|
||||||
|
const network = networkByAgent[a.id] ?? null;
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={a.id}
|
key={a.id}
|
||||||
@ -379,6 +421,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
|||||||
{delegationSource}
|
{delegationSource}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{network && <NetworkPermissionBadge state={network} />}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted">{profileName}</span>
|
<span className="text-xs text-muted">{profileName}</span>
|
||||||
{live && (
|
{live && (
|
||||||
@ -513,6 +556,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
|||||||
onSessionId={(sid) =>
|
onSessionId={(sid) =>
|
||||||
setAgentSessions((prev) => ({ ...prev, [activeAgentId]: sid }))
|
setAgentSessions((prev) => ({ ...prev, [activeAgentId]: sid }))
|
||||||
}
|
}
|
||||||
|
systemPermissions={networkByAgent[activeAgentId] ?? null}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -667,3 +711,40 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
|||||||
</Panel>
|
</Panel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NetworkPermissionBadge({
|
||||||
|
state,
|
||||||
|
}: {
|
||||||
|
state: ResolvedAgentSystemPermissions;
|
||||||
|
}) {
|
||||||
|
const locked = state.runtimeLock.state === "locked";
|
||||||
|
const label = locked
|
||||||
|
? "Verrouillé"
|
||||||
|
: state.effective === "allow"
|
||||||
|
? "Autorisé"
|
||||||
|
: state.effective === "deny"
|
||||||
|
? "Interdit"
|
||||||
|
: "Demande";
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-label={`network ${label}`}
|
||||||
|
title={
|
||||||
|
locked
|
||||||
|
? (state.runtimeLock.reason ?? "Réseau verrouillé par le runtime")
|
||||||
|
: `État effectif : ${label}`
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||||
|
locked
|
||||||
|
? "bg-warning/15 text-warning"
|
||||||
|
: state.effective === "allow"
|
||||||
|
? "bg-success/15 text-success"
|
||||||
|
: state.effective === "deny"
|
||||||
|
? "bg-danger/15 text-danger"
|
||||||
|
: "bg-primary/15 text-primary",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Réseau: {label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
MockAgentGateway,
|
MockAgentGateway,
|
||||||
|
MockPermissionGateway,
|
||||||
MockProfileGateway,
|
MockProfileGateway,
|
||||||
MockSystemGateway,
|
MockSystemGateway,
|
||||||
MockTemplateGateway,
|
MockTemplateGateway,
|
||||||
@ -44,13 +45,15 @@ function renderPanel(
|
|||||||
profile: MockProfileGateway = new MockProfileGateway(),
|
profile: MockProfileGateway = new MockProfileGateway(),
|
||||||
projectRoot = "/home/me/proj",
|
projectRoot = "/home/me/proj",
|
||||||
template?: MockTemplateGateway,
|
template?: MockTemplateGateway,
|
||||||
|
permission: MockPermissionGateway = new MockPermissionGateway(),
|
||||||
) {
|
) {
|
||||||
const tmpl = template ?? new MockTemplateGateway(agent);
|
const tmpl = template ?? new MockTemplateGateway(agent);
|
||||||
const gateways = { agent, profile, template: tmpl } as unknown as Gateways;
|
const gateways = { agent, profile, template: tmpl, permission } as unknown as Gateways;
|
||||||
return {
|
return {
|
||||||
agent,
|
agent,
|
||||||
profile,
|
profile,
|
||||||
template: tmpl,
|
template: tmpl,
|
||||||
|
permission,
|
||||||
...render(
|
...render(
|
||||||
<DIProvider gateways={gateways}>
|
<DIProvider gateways={gateways}>
|
||||||
<AgentsPanel projectId={PROJECT_ID} projectRoot={projectRoot} />
|
<AgentsPanel projectId={PROJECT_ID} projectRoot={projectRoot} />
|
||||||
@ -108,6 +111,27 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
|||||||
expect(agent).toBeTruthy();
|
expect(agent).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows the resolved network permission badge for an agent", async () => {
|
||||||
|
const agent = new MockAgentGateway();
|
||||||
|
const permission = new MockPermissionGateway();
|
||||||
|
const created = await agent.createAgent(PROJECT_ID, {
|
||||||
|
name: "Builder",
|
||||||
|
profileId: "p1",
|
||||||
|
});
|
||||||
|
permission.setResolvedAgentSystemPermissions(created.id, {
|
||||||
|
wanted: "allow",
|
||||||
|
effective: "deny",
|
||||||
|
runtimeLock: { state: "locked", reason: "Runtime locked." },
|
||||||
|
control: { mode: "readOnly", reason: "Runtime locked." },
|
||||||
|
});
|
||||||
|
|
||||||
|
renderPanel(agent, new MockProfileGateway(), "/home/me/proj", undefined, permission);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Réseau: Verrouillé")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("the Create button is disabled when the name is empty", async () => {
|
it("the Create button is disabled when the name is empty", async () => {
|
||||||
renderPanel();
|
renderPanel();
|
||||||
await waitForIdle();
|
await waitForIdle();
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Button, Panel, Spinner, cn } from "@/shared";
|
import { Button, Panel, Spinner, cn } from "@/shared";
|
||||||
import type { PermissionSet, PermissionPosture } from "@/domain";
|
import type {
|
||||||
|
NetworkPolicy,
|
||||||
|
PermissionSet,
|
||||||
|
PermissionPosture,
|
||||||
|
ResolvedAgentSystemPermissions,
|
||||||
|
SystemPermissionSet,
|
||||||
|
} from "@/domain";
|
||||||
import { McpToolPermissionsPanel } from "./McpToolPermissionsPanel";
|
import { McpToolPermissionsPanel } from "./McpToolPermissionsPanel";
|
||||||
import {
|
import {
|
||||||
type CapabilityChoice,
|
type CapabilityChoice,
|
||||||
@ -42,6 +48,12 @@ const POSTURE_LABELS: Record<PermissionPosture, string> = {
|
|||||||
deny: "Deny",
|
deny: "Deny",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NETWORK_LABELS: Record<NetworkPolicy, string> = {
|
||||||
|
allow: "Autorisé",
|
||||||
|
deny: "Interdit",
|
||||||
|
ask: "Demande",
|
||||||
|
};
|
||||||
|
|
||||||
export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
||||||
const vm = usePermissions(projectId);
|
const vm = usePermissions(projectId);
|
||||||
const [target, setTarget] = useState<EditorTarget>({ type: "project" });
|
const [target, setTarget] = useState<EditorTarget>({ type: "project" });
|
||||||
@ -51,6 +63,16 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
|||||||
? vm.rows.find((row) => row.agent.id === target.agentId) ?? null
|
? vm.rows.find((row) => row.agent.id === target.agentId) ?? null
|
||||||
: null;
|
: null;
|
||||||
const activePolicy = selectedAgent?.override ?? vm.document?.projectDefaults ?? null;
|
const activePolicy = selectedAgent?.override ?? vm.document?.projectDefaults ?? null;
|
||||||
|
const activeSystemSet =
|
||||||
|
target.type === "project"
|
||||||
|
? vm.systemDocument?.projectDefault ?? null
|
||||||
|
: selectedAgent?.systemOverride ??
|
||||||
|
vm.systemDocument?.projectDefault ??
|
||||||
|
null;
|
||||||
|
const activeResolvedSystem =
|
||||||
|
target.type === "project"
|
||||||
|
? vm.rows[0]?.resolvedSystem ?? null
|
||||||
|
: selectedAgent?.resolvedSystem ?? null;
|
||||||
const activeDraft = target.type === "project"
|
const activeDraft = target.type === "project"
|
||||||
? vm.projectDraft
|
? vm.projectDraft
|
||||||
: draftFromSet(selectedAgent?.override ?? vm.document?.projectDefaults ?? null);
|
: draftFromSet(selectedAgent?.override ?? vm.document?.projectDefaults ?? null);
|
||||||
@ -75,6 +97,14 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSaveNetwork(permissions: SystemPermissionSet | null) {
|
||||||
|
if (target.type === "project") {
|
||||||
|
await vm.saveProjectSystemPermissions(permissions);
|
||||||
|
} else {
|
||||||
|
await vm.saveAgentSystemPermissions(target.agentId, permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
title="Permissions"
|
title="Permissions"
|
||||||
@ -202,6 +232,24 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
|||||||
onSave={(draft) => void handleSave(draft)}
|
onSave={(draft) => void handleSave(draft)}
|
||||||
onClear={() => void handleClear()}
|
onClear={() => void handleClear()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<NetworkPermissionEditor
|
||||||
|
key={`network-${target.type === "project" ? "project" : target.agentId}`}
|
||||||
|
title={
|
||||||
|
target.type === "project"
|
||||||
|
? "Réseau — defaults projet"
|
||||||
|
: `Réseau — ${target.agentName}`
|
||||||
|
}
|
||||||
|
source={activeSystemSet}
|
||||||
|
resolved={activeResolvedSystem}
|
||||||
|
inherited={
|
||||||
|
target.type === "agent" && selectedAgent?.systemOverride == null
|
||||||
|
? vm.systemDocument?.projectDefault ?? null
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
busy={vm.busy}
|
||||||
|
onSave={(permissions) => void handleSaveNetwork(permissions)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -267,6 +315,138 @@ function PolicyCard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NetworkPermissionEditorProps {
|
||||||
|
title: string;
|
||||||
|
source: SystemPermissionSet | null;
|
||||||
|
inherited: SystemPermissionSet | null;
|
||||||
|
resolved: ResolvedAgentSystemPermissions | null;
|
||||||
|
busy: boolean;
|
||||||
|
onSave: (permissions: SystemPermissionSet | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NetworkPermissionEditor({
|
||||||
|
title,
|
||||||
|
source,
|
||||||
|
inherited,
|
||||||
|
resolved,
|
||||||
|
busy,
|
||||||
|
onSave,
|
||||||
|
}: NetworkPermissionEditorProps) {
|
||||||
|
const current = source?.network ?? "";
|
||||||
|
const [local, setLocal] = useState<NetworkPolicy | "">(current);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocal(current);
|
||||||
|
}, [current]);
|
||||||
|
|
||||||
|
const readOnly = resolved?.control.mode === "readOnly";
|
||||||
|
const changed = local !== current;
|
||||||
|
const wanted = resolved?.wanted ?? source?.network ?? inherited?.network ?? null;
|
||||||
|
const effective = resolved?.effective ?? null;
|
||||||
|
const runtimeLocked = resolved?.runtimeLock.state === "locked";
|
||||||
|
const lockReason = resolved?.runtimeLock.reason ?? resolved?.control.reason ?? null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="rounded-md border border-border bg-surface">
|
||||||
|
<header className="border-b border-border px-3 py-2.5">
|
||||||
|
<h4 className="text-sm font-semibold text-content">{title}</h4>
|
||||||
|
<p className="text-xs text-muted">
|
||||||
|
Politique voulue, état effectif et verrou runtime.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 p-3">
|
||||||
|
<div className="grid grid-cols-1 gap-2 text-xs sm:grid-cols-2">
|
||||||
|
<StatusLine
|
||||||
|
label="Wanted"
|
||||||
|
value={wanted ? `Réseau ${NETWORK_LABELS[wanted].toLowerCase()}` : "Non configuré"}
|
||||||
|
/>
|
||||||
|
<StatusLine
|
||||||
|
label="Effective"
|
||||||
|
value={
|
||||||
|
effective
|
||||||
|
? `Réseau ${NETWORK_LABELS[effective].toLowerCase()}`
|
||||||
|
: "Indisponible"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusLine
|
||||||
|
label="Runtime"
|
||||||
|
value={runtimeLocked ? "Verrouillé par le runtime" : "Aucun verrou connu"}
|
||||||
|
/>
|
||||||
|
<StatusLine
|
||||||
|
label="Control"
|
||||||
|
value={readOnly ? "Lecture seule" : "Modifiable"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{readOnly && (
|
||||||
|
<p className="rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||||
|
{lockReason ?? "Le runtime actif ne permet pas de modifier cette permission."}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label className="flex flex-col gap-1">
|
||||||
|
<span className="text-xs font-medium text-muted">Réseau</span>
|
||||||
|
<select
|
||||||
|
aria-label={`${title} network`}
|
||||||
|
value={local}
|
||||||
|
disabled={busy || readOnly}
|
||||||
|
onChange={(e) => setLocal(e.target.value as NetworkPolicy | "")}
|
||||||
|
className={cn(
|
||||||
|
"h-9 rounded-md border border-border bg-raised px-3 text-sm text-content",
|
||||||
|
"outline-none transition-colors focus:border-primary disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<option value="">Non configuré</option>
|
||||||
|
<option value="allow">Réseau autorisé</option>
|
||||||
|
<option value="deny">Réseau interdit</option>
|
||||||
|
<option value="ask">Demande d'autorisation</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{inherited?.network && !source?.network && (
|
||||||
|
<p className="text-xs text-muted">
|
||||||
|
Hérite du projet : Réseau {NETWORK_LABELS[inherited.network].toLowerCase()}.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2 pt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="Clear network"
|
||||||
|
disabled={busy || readOnly || !source?.network}
|
||||||
|
onClick={() => onSave(null)}
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="primary"
|
||||||
|
aria-label="Save network"
|
||||||
|
disabled={busy || readOnly || !changed}
|
||||||
|
loading={busy && changed}
|
||||||
|
onClick={() => onSave(local ? { network: local } : null)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusLine({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-border/70 bg-raised px-3 py-2">
|
||||||
|
<span className="block text-[11px] font-medium uppercase text-faint">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-content">{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface PermissionEditorProps {
|
interface PermissionEditorProps {
|
||||||
title: string;
|
title: string;
|
||||||
draft: PolicyDraft;
|
draft: PolicyDraft;
|
||||||
|
|||||||
@ -86,4 +86,58 @@ describe("PermissionsPanel", () => {
|
|||||||
expect(doc.agents).toEqual([]);
|
expect(doc.agents).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("saves project network defaults in the separate system permissions document", async () => {
|
||||||
|
const { permission } = await renderPanel();
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText("Réseau — defaults projet network"), {
|
||||||
|
target: { value: "allow" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save network" }));
|
||||||
|
|
||||||
|
await waitFor(async () => {
|
||||||
|
const doc = await permission.getProjectSystemPermissions(PROJECT_ID);
|
||||||
|
expect(doc.projectDefault?.network).toBe("allow");
|
||||||
|
});
|
||||||
|
expect(screen.getAllByText("Réseau autorisé").length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the network control read-only when the resolved runtime says so", async () => {
|
||||||
|
const agent = new MockAgentGateway();
|
||||||
|
const permission = new MockPermissionGateway();
|
||||||
|
const created = await agent.createAgent(PROJECT_ID, {
|
||||||
|
name: "Builder",
|
||||||
|
profileId: "p1",
|
||||||
|
});
|
||||||
|
permission.setResolvedAgentSystemPermissions(created.id, {
|
||||||
|
wanted: "allow",
|
||||||
|
effective: "deny",
|
||||||
|
runtimeLock: {
|
||||||
|
state: "locked",
|
||||||
|
source: "external-runtime",
|
||||||
|
reason: "Runtime network is locked.",
|
||||||
|
},
|
||||||
|
control: { mode: "readOnly", reason: "Runtime network is locked." },
|
||||||
|
});
|
||||||
|
const gateways = {
|
||||||
|
agent,
|
||||||
|
permission,
|
||||||
|
profile: new MockProfileGateway(),
|
||||||
|
} as unknown as Gateways;
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider gateways={gateways}>
|
||||||
|
<PermissionsPanel projectId={PROJECT_ID} />
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText("Verrouillé par le runtime")).toBeTruthy();
|
||||||
|
});
|
||||||
|
expect(screen.getByText("Runtime network is locked.")).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
(screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement)
|
||||||
|
.disabled,
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import type {
|
|||||||
PermissionRule,
|
PermissionRule,
|
||||||
PermissionSet,
|
PermissionSet,
|
||||||
ProjectPermissions,
|
ProjectPermissions,
|
||||||
|
ProjectSystemPermissions,
|
||||||
|
ResolvedAgentSystemPermissions,
|
||||||
|
SystemPermissionSet,
|
||||||
} from "@/domain";
|
} from "@/domain";
|
||||||
|
|
||||||
export type CapabilityChoice = "none" | PermissionEffect;
|
export type CapabilityChoice = "none" | PermissionEffect;
|
||||||
@ -24,12 +27,15 @@ export interface PolicyDraft {
|
|||||||
export interface AgentPermissionRow {
|
export interface AgentPermissionRow {
|
||||||
agent: Agent;
|
agent: Agent;
|
||||||
override: PermissionSet | null;
|
override: PermissionSet | null;
|
||||||
|
systemOverride: SystemPermissionSet | null;
|
||||||
|
resolvedSystem: ResolvedAgentSystemPermissions | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PermissionsViewModel {
|
export interface PermissionsViewModel {
|
||||||
agents: Agent[];
|
agents: Agent[];
|
||||||
rows: AgentPermissionRow[];
|
rows: AgentPermissionRow[];
|
||||||
document: ProjectPermissions | null;
|
document: ProjectPermissions | null;
|
||||||
|
systemDocument: ProjectSystemPermissions | null;
|
||||||
projectDraft: PolicyDraft;
|
projectDraft: PolicyDraft;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
@ -38,6 +44,13 @@ export interface PermissionsViewModel {
|
|||||||
clearProjectDefaults: () => Promise<void>;
|
clearProjectDefaults: () => Promise<void>;
|
||||||
saveAgentOverride: (agentId: string, draft: PolicyDraft) => Promise<void>;
|
saveAgentOverride: (agentId: string, draft: PolicyDraft) => Promise<void>;
|
||||||
clearAgentOverride: (agentId: string) => Promise<void>;
|
clearAgentOverride: (agentId: string) => Promise<void>;
|
||||||
|
saveProjectSystemPermissions: (
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
) => Promise<void>;
|
||||||
|
saveAgentSystemPermissions: (
|
||||||
|
agentId: string,
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_DRAFT: PolicyDraft = {
|
const DEFAULT_DRAFT: PolicyDraft = {
|
||||||
@ -114,6 +127,11 @@ export function usePermissions(projectId: string): PermissionsViewModel {
|
|||||||
const { agent, permission } = useGateways();
|
const { agent, permission } = useGateways();
|
||||||
const [agents, setAgents] = useState<Agent[]>([]);
|
const [agents, setAgents] = useState<Agent[]>([]);
|
||||||
const [document, setDocument] = useState<ProjectPermissions | null>(null);
|
const [document, setDocument] = useState<ProjectPermissions | null>(null);
|
||||||
|
const [systemDocument, setSystemDocument] =
|
||||||
|
useState<ProjectSystemPermissions | null>(null);
|
||||||
|
const [resolvedSystemByAgent, setResolvedSystemByAgent] = useState<
|
||||||
|
Record<string, ResolvedAgentSystemPermissions>
|
||||||
|
>({});
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
@ -121,12 +139,34 @@ export function usePermissions(projectId: string): PermissionsViewModel {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const [agentList, permissionDoc] = await Promise.all([
|
const [agentList, permissionDoc, systemPermissionDoc] = await Promise.all([
|
||||||
agent.listAgents(projectId),
|
agent.listAgents(projectId),
|
||||||
permission.getProjectPermissions(projectId),
|
permission.getProjectPermissions(projectId),
|
||||||
|
permission.getProjectSystemPermissions(projectId),
|
||||||
]);
|
]);
|
||||||
|
const resolvedPairs = await Promise.all(
|
||||||
|
agentList.map(async (candidate) => {
|
||||||
|
try {
|
||||||
|
return [
|
||||||
|
candidate.id,
|
||||||
|
await permission.resolveAgentSystemPermissions(projectId, candidate.id),
|
||||||
|
] as const;
|
||||||
|
} catch {
|
||||||
|
return [candidate.id, null] as const;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
setAgents(agentList);
|
setAgents(agentList);
|
||||||
setDocument(permissionDoc);
|
setDocument(permissionDoc);
|
||||||
|
setSystemDocument(systemPermissionDoc);
|
||||||
|
setResolvedSystemByAgent(
|
||||||
|
Object.fromEntries(
|
||||||
|
resolvedPairs.filter(
|
||||||
|
(pair): pair is readonly [string, ResolvedAgentSystemPermissions] =>
|
||||||
|
pair[1] !== null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(describe(e));
|
setError(describe(e));
|
||||||
} finally {
|
} finally {
|
||||||
@ -142,11 +182,19 @@ export function usePermissions(projectId: string): PermissionsViewModel {
|
|||||||
const overrides = new Map(
|
const overrides = new Map(
|
||||||
(document?.agents ?? []).map((entry) => [entry.agentId, entry.permissions]),
|
(document?.agents ?? []).map((entry) => [entry.agentId, entry.permissions]),
|
||||||
);
|
);
|
||||||
|
const systemOverrides = new Map(
|
||||||
|
(systemDocument?.agents ?? []).map((entry) => [
|
||||||
|
entry.agentId,
|
||||||
|
entry.permissions,
|
||||||
|
]),
|
||||||
|
);
|
||||||
return agents.map((candidate) => ({
|
return agents.map((candidate) => ({
|
||||||
agent: candidate,
|
agent: candidate,
|
||||||
override: overrides.get(candidate.id) ?? null,
|
override: overrides.get(candidate.id) ?? null,
|
||||||
|
systemOverride: systemOverrides.get(candidate.id) ?? null,
|
||||||
|
resolvedSystem: resolvedSystemByAgent[candidate.id] ?? null,
|
||||||
}));
|
}));
|
||||||
}, [agents, document]);
|
}, [agents, document, systemDocument, resolvedSystemByAgent]);
|
||||||
|
|
||||||
const projectDraft = useMemo(
|
const projectDraft = useMemo(
|
||||||
() => draftFromSet(document?.projectDefaults ?? null),
|
() => draftFromSet(document?.projectDefaults ?? null),
|
||||||
@ -220,10 +268,52 @@ export function usePermissions(projectId: string): PermissionsViewModel {
|
|||||||
[permission, projectId],
|
[permission, projectId],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const saveProjectSystemPermissions = useCallback(
|
||||||
|
async (permissionsToSave: SystemPermissionSet | null) => {
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const next = await permission.updateProjectSystemPermissions(
|
||||||
|
projectId,
|
||||||
|
permissionsToSave,
|
||||||
|
);
|
||||||
|
setSystemDocument(next);
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
setError(describe(e));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[permission, projectId, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveAgentSystemPermissions = useCallback(
|
||||||
|
async (agentId: string, permissionsToSave: SystemPermissionSet | null) => {
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const next = await permission.updateAgentSystemPermissions(
|
||||||
|
projectId,
|
||||||
|
agentId,
|
||||||
|
permissionsToSave,
|
||||||
|
);
|
||||||
|
setSystemDocument(next);
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
setError(describe(e));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[permission, projectId, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
agents,
|
agents,
|
||||||
rows,
|
rows,
|
||||||
document,
|
document,
|
||||||
|
systemDocument,
|
||||||
projectDraft,
|
projectDraft,
|
||||||
busy,
|
busy,
|
||||||
error,
|
error,
|
||||||
@ -232,5 +322,7 @@ export function usePermissions(projectId: string): PermissionsViewModel {
|
|||||||
clearProjectDefaults,
|
clearProjectDefaults,
|
||||||
saveAgentOverride,
|
saveAgentOverride,
|
||||||
clearAgentOverride,
|
clearAgentOverride,
|
||||||
|
saveProjectSystemPermissions,
|
||||||
|
saveAgentSystemPermissions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,6 +55,22 @@ describe("TerminalView (with MockTerminalGateway)", () => {
|
|||||||
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows a non-blocking network banner when system permissions are locked", () => {
|
||||||
|
renderView(new MockTerminalGateway(), "/home/me/proj", {
|
||||||
|
systemPermissions: {
|
||||||
|
wanted: "allow",
|
||||||
|
effective: "deny",
|
||||||
|
runtimeLock: { state: "locked", reason: "Runtime network locked." },
|
||||||
|
control: { mode: "readOnly", reason: "Runtime network locked." },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByTestId("terminal-network-banner").textContent).toContain(
|
||||||
|
"Réseau verrouillé par le runtime.",
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("opens a terminal through the gateway with the given cwd", async () => {
|
it("opens a terminal through the gateway with the given cwd", async () => {
|
||||||
const gw = new MockTerminalGateway();
|
const gw = new MockTerminalGateway();
|
||||||
const openSpy = vi.spyOn(gw, "openTerminal");
|
const openSpy = vi.spyOn(gw, "openTerminal");
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import { FitAddon } from "@xterm/addon-fit";
|
|||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
import { useGateways } from "@/app/di";
|
import { useGateways } from "@/app/di";
|
||||||
|
import type { ResolvedAgentSystemPermissions } from "@/domain";
|
||||||
import type {
|
import type {
|
||||||
OpenTerminalOptions,
|
OpenTerminalOptions,
|
||||||
ReattachResult,
|
ReattachResult,
|
||||||
@ -113,6 +114,8 @@ interface TerminalViewProps {
|
|||||||
* it never remounts/reopens the terminal.
|
* it never remounts/reopens the terminal.
|
||||||
*/
|
*/
|
||||||
refitSignal?: number;
|
refitSignal?: number;
|
||||||
|
/** Optional resolved system permissions for this agent/cell. */
|
||||||
|
systemPermissions?: ResolvedAgentSystemPermissions | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -144,6 +147,7 @@ export function TerminalView({
|
|||||||
portal,
|
portal,
|
||||||
onReady,
|
onReady,
|
||||||
refitSignal,
|
refitSignal,
|
||||||
|
systemPermissions,
|
||||||
}: TerminalViewProps) {
|
}: TerminalViewProps) {
|
||||||
const { terminal } = useGateways();
|
const { terminal } = useGateways();
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -414,6 +418,15 @@ export function TerminalView({
|
|||||||
refitRef.current?.();
|
refitRef.current?.();
|
||||||
}, [refitSignal]);
|
}, [refitSignal]);
|
||||||
|
|
||||||
|
const showNetworkBanner =
|
||||||
|
systemPermissions != null &&
|
||||||
|
(systemPermissions.runtimeLock.state === "locked" ||
|
||||||
|
systemPermissions.effective === "deny");
|
||||||
|
const networkReason =
|
||||||
|
systemPermissions?.runtimeLock.reason ??
|
||||||
|
systemPermissions?.control.reason ??
|
||||||
|
"Le réseau est interdit pour cette cellule.";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="terminal-view"
|
data-testid="terminal-view"
|
||||||
@ -427,6 +440,34 @@ export function TerminalView({
|
|||||||
{/* xterm mounts into this inner node; the error banner is a sibling so
|
{/* xterm mounts into this inner node; the error banner is a sibling so
|
||||||
React never fights xterm over the same subtree. */}
|
React never fights xterm over the same subtree. */}
|
||||||
<div ref={containerRef} style={{ width: "100%", height: "100%" }} />
|
<div ref={containerRef} style={{ width: "100%", height: "100%" }} />
|
||||||
|
{showNetworkBanner && (
|
||||||
|
<div
|
||||||
|
role="status"
|
||||||
|
data-testid="terminal-network-banner"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 8,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
padding: "0.5rem 0.75rem",
|
||||||
|
border: "1px solid rgba(245, 158, 11, 0.45)",
|
||||||
|
borderRadius: 6,
|
||||||
|
background: "rgba(24, 24, 27, 0.94)",
|
||||||
|
color: "var(--color-warning, #f59e0b)",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily:
|
||||||
|
'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{systemPermissions.runtimeLock.state === "locked"
|
||||||
|
? "Réseau verrouillé par le runtime."
|
||||||
|
: "Réseau interdit pour cet agent."}{" "}
|
||||||
|
{networkReason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{openError && (
|
{openError && (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
|
|||||||
@ -52,13 +52,16 @@ import type {
|
|||||||
Project,
|
Project,
|
||||||
ProjectPermissions,
|
ProjectPermissions,
|
||||||
ProjectWorkState,
|
ProjectWorkState,
|
||||||
|
ProjectSystemPermissions,
|
||||||
ProfileAvailability,
|
ProfileAvailability,
|
||||||
ResumableAgent,
|
ResumableAgent,
|
||||||
|
ResolvedAgentSystemPermissions,
|
||||||
ReplyChunk,
|
ReplyChunk,
|
||||||
ServerExposurePreview,
|
ServerExposurePreview,
|
||||||
ServerExposureSettings,
|
ServerExposureSettings,
|
||||||
Skill,
|
Skill,
|
||||||
SkillScope,
|
SkillScope,
|
||||||
|
SystemPermissionSet,
|
||||||
Sprint,
|
Sprint,
|
||||||
Template,
|
Template,
|
||||||
TerminalSession,
|
TerminalSession,
|
||||||
@ -861,6 +864,24 @@ export interface PermissionGateway {
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
agentId: string,
|
agentId: string,
|
||||||
): Promise<EffectivePermissions | null>;
|
): Promise<EffectivePermissions | null>;
|
||||||
|
/** Reads the full project system-permission document. */
|
||||||
|
getProjectSystemPermissions(projectId: string): Promise<ProjectSystemPermissions>;
|
||||||
|
/** Replaces or removes project-level default system permissions. */
|
||||||
|
updateProjectSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
): Promise<ProjectSystemPermissions>;
|
||||||
|
/** Replaces or removes one agent-specific system-permission override. */
|
||||||
|
updateAgentSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
permissions: SystemPermissionSet | null,
|
||||||
|
): Promise<ProjectSystemPermissions>;
|
||||||
|
/** Resolves wanted/effective runtime-constrained system permissions. */
|
||||||
|
resolveAgentSystemPermissions(
|
||||||
|
projectId: string,
|
||||||
|
agentId: string,
|
||||||
|
): Promise<ResolvedAgentSystemPermissions>;
|
||||||
/**
|
/**
|
||||||
* Reads the project's durable MCP tool permission document plus the
|
* Reads the project's durable MCP tool permission document plus the
|
||||||
* backend-canonical catalogue classification (ticket #82). Distinct
|
* backend-canonical catalogue classification (ticket #82). Distinct
|
||||||
|
|||||||
Reference in New Issue
Block a user