feat(permissions): expose network permission state (#103)

This commit is contained in:
2026-07-25 23:05:55 +02:00
parent e8731834f4
commit 3047dc9195
31 changed files with 2080 additions and 94 deletions

View File

@ -14,19 +14,21 @@ use application::{
CloseProjectInput, CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput,
DeleteAgentInput, DeleteEmbedderProfileInput, DeleteLayoutInput, DeleteMemoryInput,
DeleteSkillInput, DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput,
GetProjectWorkStateInput, GitBranchesInput, GitCheckoutInput, GitCommitInput, GitGraphInput,
GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput, InspectConversationInput,
LaunchAgentInput, ListAgentsInput, ListDevicesInput, ListLayoutsInput, ListMemoriesInput,
ListResumableAgentsInput, ListSkillsInput, LiveSessions, LoadLayoutInput, McpRuntime,
MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadConversationPageInput,
ReadMcpToolPermissionsInput, ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput,
ReconcileLayoutsInput, ReconcileLiveStateInput, RenameDeviceInput, RenameLayoutInput,
ResolveAgentPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
GetProjectSystemPermissionsInput, GetProjectWorkStateInput, GitBranchesInput, GitCheckoutInput,
GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput,
InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListDevicesInput,
ListLayoutsInput, ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, LiveSessions,
LoadLayoutInput, McpRuntime, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput,
ReadConversationPageInput, ReadMcpToolPermissionsInput, ReadMemoryIndexInput,
ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput, ReconcileLiveStateInput,
RenameDeviceInput, RenameLayoutInput, ResolveAgentPermissionsInput,
ResolveAgentSystemPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
RotateConversationLogInput, SetActiveLayoutInput, SnapshotRunningAgentsInput,
StopLiveAgentInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
UpdateAgentContextInput, UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
UpdateMemoryInput, UpdateProjectContextInput, UpdateProjectMcpToolPermissionsInput,
UpdateProjectPermissionsInput, UpdateSkillInput,
UpdateAgentSystemPermissionsInput, UpdateMemoryInput, UpdateProjectContextInput,
UpdateProjectMcpToolPermissionsInput, UpdateProjectPermissionsInput,
UpdateProjectSystemPermissionsInput, UpdateSkillInput,
};
use domain::ports::ModelServerRuntime;
use domain::ports::PtyHandle;
@ -52,19 +54,22 @@ use crate::dto::{
MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto, ModelServerConfigDto,
ModelServerConfigListDto, OpenCodeProviderListDto, OpenTerminalRequestDto,
PreviewModelServerCommandDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectWorkStateDto,
ReadAgentContextResponseDto, ReadConversationPageRequestDto, ReattachChatDto,
ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto, ResumableAgentListDto,
SaveEmbedderProfileRequestDto, SaveModelServerRequestDto,
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSystemPermissionsDto,
ProjectWorkStateDto, ReadAgentContextResponseDto, ReadConversationPageRequestDto,
ReattachChatDto, ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto,
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveModelServerRequestDto,
SaveOpenCodeProviderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto,
SetActiveLayoutResultDto, SkillDto, SkillListDto, StopLiveAgentRequestDto,
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
TemplateListDto, TerminalClosedDto, TerminalSessionDto, TurnPageDto, UnassignSkillRequestDto,
UpdateAgentContextRequestDto, UpdateAgentMcpToolPermissionsRequestDto,
UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
UpdateAgentPermissionsRequestDto, UpdateAgentSystemPermissionsRequestDto,
UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
UpdateProjectMcpToolPermissionsRequestDto, UpdateProjectPermissionsRequestDto,
UpdateSkillRequestDto, UpdateTemplateRequestDto, WriteTerminalRequestDto,
UpdateProjectSystemPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
WriteTerminalRequestDto,
};
use crate::embedded_server::{
EmbeddedServerStatusDto, ServerExposurePreviewDto, ServerExposureSettingsDto,
@ -549,6 +554,87 @@ pub async fn resolve_agent_permissions(
.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.
///
/// # Errors

View File

@ -229,6 +229,10 @@ pub fn run() {
commands::update_project_permissions,
commands::update_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::update_project_mcp_tool_permissions,
commands::update_agent_mcp_tool_permissions,

View 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");
}

View File

@ -32,6 +32,7 @@ pub mod project;
pub mod remote;
pub mod skill;
pub mod sprints;
pub mod system_permissions;
pub mod template;
pub mod terminal;
pub mod ticket_assistant;
@ -170,6 +171,13 @@ pub use sprints::{
RenameSprintInput, ReorderSprints, ReorderSprintsInput, ReorderSprintsOutput, SprintListEntry,
SprintOutput, UnassignTicketFromSprint, UnassignTicketFromSprintInput,
};
pub use system_permissions::{
GetProjectSystemPermissions, GetProjectSystemPermissionsInput,
GetProjectSystemPermissionsOutput, ResolveAgentSystemPermissions,
ResolveAgentSystemPermissionsInput, ResolveAgentSystemPermissionsOutput,
UpdateAgentSystemPermissions, UpdateAgentSystemPermissionsInput,
UpdateProjectSystemPermissions, UpdateProjectSystemPermissionsInput,
};
pub use template::{
AgentDrift, CreateAgentFromTemplate, CreateAgentFromTemplateInput,
CreateAgentFromTemplateOutput, CreateTemplate, CreateTemplateInput, CreateTemplateOutput,

View 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,
}

View 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
);
}

View File

@ -16,7 +16,10 @@ use application::{
ListProjectsOutput, LiveSessionKind, LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState,
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::*;
@ -1889,6 +1892,16 @@ pub struct ProjectPermissionsDto(pub ProjectPermissions);
#[serde(transparent)]
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.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -1959,6 +1972,38 @@ pub struct ResolveAgentPermissionsRequestDto {
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.
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]

View File

@ -22,33 +22,35 @@ use application::{
DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint, DeleteTemplate,
DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles, DismissEmbedderSuggestion,
EnsureLocalModelServer, FirstRunState, GetAppExitWorkGuardState, GetLiveStateLean, GetMemory,
GetProjectPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph,
GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase,
InspectConversation, InstallPluginFromArchive, InstallPluginFromDirectory,
JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents,
ListAgentsInput, ListDevices, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories,
ListModelServers, ListOpenCodeProviders, ListPluginRuntimeContributions, ListPlugins,
ListProfiles, ListProjects, ListResumableAgents, ListSkills, ListSprints, ListTemplates,
LiveAgentRegistry, LiveSessions, LiveStateLeanProvider, LiveStateProvider,
LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue, MoveTabToNewWindow,
MutateLayout, OnnxModelView, OpenProject, OpenTerminal, OpenTicketAssistant,
OrchestratorService, PairAttemptLimiter, PairDevice, PermissionProjectorRegistry,
ProposeContext, ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue,
ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory, ReadMemoryIndex, ReadProjectContext,
ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts, ReconcileLiveState,
ReconcileLiveStateInput, ReconcilePluginMcpServers, RecordTurn, RecordTurnProvider,
ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal,
ResolveAgentPermissions, ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask,
ReviewPluginPackage, RevokeAllDevices, RevokeDevice, RotateConversationLog,
SaveEmbedderProfile, SaveModelServer, SaveOpenCodeProviderProfile, SaveProfile,
SessionLimitService, SetActiveLayout, SetPluginEnabled, SnapshotOpenWindows,
SnapshotRunningAgents, SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode,
StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice,
UnassignSkillFromAgent, UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues,
UpdateAgentContext, UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateIssue,
UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateSkill, UpdateTemplate,
WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
GetProjectPermissions, GetProjectSystemPermissions, GetProjectWorkState, GitBranches,
GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage,
HarvestMemoryFromTurn, HealthUseCase, InspectConversation, InstallPluginFromArchive,
InstallPluginFromDirectory, JsonPluginManifestValidator, LaunchAgent, LaunchAgentInput,
LinkIssues, ListAgents, ListAgentsInput, ListDevices, ListEmbedderProfiles, ListIssues,
ListLayouts, ListMemories, ListModelServers, ListOpenCodeProviders,
ListPluginRuntimeContributions, ListPlugins, ListProfiles, ListProjects, ListResumableAgents,
ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions, LiveStateLeanProvider,
LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue,
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory,
ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
ReconcileLiveState, ReconcileLiveStateInput, ReconcilePluginMcpServers, RecordTurn,
RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint,
ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveAgentSystemPermissions,
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage,
RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveEmbedderProfile, SaveModelServer,
SaveOpenCodeProviderProfile, SaveProfile, SessionLimitService, SetActiveLayout,
SetPluginEnabled, SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand,
StopLiveAgent, StructuredRoutingMode, StructuredSessions, SuggestedThisSession,
SyncAgentWithTemplate, TerminalSessions, TouchDevice, UnassignSkillFromAgent,
UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues, UpdateAgentContext,
UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateAgentSystemPermissions,
UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateProjectSystemPermissions,
UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal,
AGENT_MEMORY_RECALL_BUDGET,
};
use async_trait::async_trait;
use domain::ports::{
@ -58,9 +60,10 @@ use domain::ports::{
EmbedderProfileStore, EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator,
IssueNumberAllocator, IssueStore, McpToolPermissionStore, MemoryRecall, MemoryStore,
PermissionStore, PluginManifestValidator, PluginMcpSupervisor, PluginPackageStore,
PluginRegistryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort, ScheduledTask,
Scheduler, SecretStore, SkillStore, SprintStore, StructuredSessionEnvironmentPreparer,
TemplateStore, ToolInvoker, WakeError, WakeReason, WindowStateStore,
PluginRegistryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyPort,
RuntimePermissionProbe, ScheduledTask, Scheduler, SecretStore, SkillStore, SprintStore,
StructuredSessionEnvironmentPreparer, SystemPermissionStore, TemplateStore, ToolInvoker,
WakeError, WakeReason, WindowStateStore,
};
use domain::profile::{
AgentProfile, ContextInjection, McpConfigStrategy, McpTransport, StructuredAdapter,
@ -83,16 +86,16 @@ use infrastructure::{
FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore, FsMcpToolPermissionStore,
FsMemoryStore, FsModelServerRegistry, FsOrchestratorWatcher, FsPermissionStore,
FsPluginPackageStore, FsPluginRegistryStore, FsProfileStore, FsProjectStore,
FsProviderSessionStore, FsSecretStore, FsSkillStore, FsSprintStore, FsTemplateStore,
FsWindowStateStore, Git2Repository, HeuristicHandoffSummarizer, HfModelArtifactDownloader,
HttpOpenAiCompatibleProbe, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
InMemoryPairAttemptLimiter, LlamaCppRuntime, LocalFileSystem, LocalManagedProcess,
LocalProcessSpawner, McpServer, MediatedInbox, NaiveMemoryRecall, OrchestratorWatchHandle,
PortablePtyAdapter, RwFileGuard, StructuredSessionFactory, SystemClock, SystemMillisClock,
TemplateToolProvider, TicketAssistantEnvironmentPreparer, TicketToolProvider,
TokioBroadcastEventBus, TokioScheduler, ToolPolicyRegistry, UuidGenerator, VectorMemoryRecall,
DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED,
VECTOR_ONNX_ENABLED,
FsProviderSessionStore, FsSecretStore, FsSkillStore, FsSprintStore, FsSystemPermissionStore,
FsTemplateStore, FsWindowStateStore, Git2Repository, HeuristicHandoffSummarizer,
HfModelArtifactDownloader, HttpOpenAiCompatibleProbe, IdeaiContextStore,
InMemoryConversationRegistry, InMemoryMailbox, InMemoryPairAttemptLimiter, LlamaCppRuntime,
LocalFileSystem, LocalManagedProcess, LocalProcessSpawner, McpServer, MediatedInbox,
NaiveMemoryRecall, OrchestratorWatchHandle, PortablePtyAdapter, ReadOnlyRuntimePermissionProbe,
RwFileGuard, StructuredSessionFactory, SystemClock, SystemMillisClock, TemplateToolProvider,
TicketAssistantEnvironmentPreparer, TicketToolProvider, TokioBroadcastEventBus, TokioScheduler,
ToolPolicyRegistry, UuidGenerator, VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL,
ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
};
pub mod dto;
@ -1063,6 +1066,14 @@ pub struct BackendCore {
pub update_agent_permissions: Arc<UpdateAgentPermissions>,
/// Resolve effective permissions for one agent.
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) ---
/// Detach a tab into a new OS window (persists the workspace topology).
pub move_tab: Arc<MoveTabToNewWindow>,
@ -1640,6 +1651,24 @@ impl BackendCore {
// --- Project permissions (LP1) ---
let permission_store = Arc::new(FsPermissionStore::new(Arc::clone(&fs_port)));
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 =
Arc::new(FsMcpToolPermissionStore::new(Arc::clone(&fs_port)));
let mcp_tool_permission_store_port =
@ -2686,6 +2715,10 @@ impl BackendCore {
update_project_permissions,
update_agent_permissions,
resolve_agent_permissions,
get_project_system_permissions,
update_project_system_permissions,
update_agent_system_permissions,
resolve_agent_system_permissions,
create_template,
read_template,
update_template,

View File

@ -64,6 +64,7 @@ pub mod sandbox;
pub mod session_limit;
pub mod skill;
pub mod sprint;
pub mod system_permissions;
pub mod template;
pub mod terminal;
@ -195,6 +196,13 @@ pub use permission::{
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::{
ContentHash, CustomPluginLayout, PluginBundleUrl, PluginCapability, PluginCommandId,
PluginContributionSet, PluginDescriptor, PluginError, PluginId, PluginInstallSource,
@ -228,7 +236,8 @@ pub use ports::{
PluginManifestError, PluginManifestValidator, PluginMcpError, PluginMcpSupervisor,
PluginPackageStore, PluginRegistryError, PluginRegistryStore, PluginStoreError,
PreparedContext, ProcessError, ProcessSpawner, ProfileStore, ProjectStore, PtyError, PtyHandle,
PtyPort, RemoteError, RemoteHost, RemotePath, RuntimeError, ScheduledTask, Scheduler,
SpawnSpec, SprintStore, SprintStoreError, StoreError, StructuredSessionEnvironment,
StructuredSessionEnvironmentPreparer, TemplateStore, WindowStateStore,
PtyPort, RemoteError, RemoteHost, RemotePath, RuntimeError, RuntimePermissionProbe,
ScheduledTask, Scheduler, SpawnSpec, SprintStore, SprintStoreError, StoreError,
StructuredSessionEnvironment, StructuredSessionEnvironmentPreparer, SystemPermissionStore,
TemplateStore, WindowStateStore,
};

View File

@ -59,6 +59,7 @@ use crate::project::{Project, ProjectPath};
use crate::remote::RemoteKind;
use crate::skill::{Skill, SkillScope};
use crate::sprint::{Sprint, SprintIndexEntry, SprintVersion};
use crate::system_permissions::{ProjectSystemPermissions, RuntimePermissionSnapshot};
use crate::template::AgentTemplate;
use crate::terminal::PtySize;
@ -2026,6 +2027,44 @@ pub trait PermissionStore: Send + Sync {
) -> 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`.
///
/// This is intentionally distinct from [`PermissionStore`]: it governs IdeA MCP

View 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);
}
}

View File

@ -36,6 +36,7 @@ pub mod pty;
pub mod ratelimit;
pub mod remote;
pub mod runtime;
pub mod runtime_permission;
pub mod sandbox;
pub mod scheduler;
pub mod session;
@ -87,6 +88,7 @@ pub use pty::PortablePtyAdapter;
pub use ratelimit::RateLimitParser;
pub use remote::{remote_host, LocalHost};
pub use runtime::CliAgentRuntime;
pub use runtime_permission::ReadOnlyRuntimePermissionProbe;
#[cfg(target_os = "linux")]
pub use sandbox::LandlockSandbox;
pub use sandbox::{default_enforcer, NoopSandbox};
@ -102,8 +104,8 @@ pub use store::{
AdaptiveMemoryRecall, BackgroundTaskReconcileReport, EmbedderEnvProbe, FsBackgroundTaskStore,
FsDeviceSessionStore, FsEmbedderProfileStore, FsEmbedderPromptStore, FsLiveStateStore,
FsMcpToolPermissionStore, FsMemoryStore, FsPermissionStore, FsProfileStore, FsProjectStore,
FsSecretStore, FsSkillStore, FsTemplateStore, FsWindowStateStore, HashEmbedder,
IdeaiContextStore, NaiveMemoryRecall, OnnxModelInfo, StubEmbedder, VectorMemoryRecall,
DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED,
VECTOR_ONNX_ENABLED,
FsSecretStore, FsSkillStore, FsSystemPermissionStore, FsTemplateStore, FsWindowStateStore,
HashEmbedder, IdeaiContextStore, NaiveMemoryRecall, OnnxModelInfo, StubEmbedder,
VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS,
VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
};

View 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())
}
}

View File

@ -16,6 +16,7 @@ mod profile;
mod project;
mod secrets;
mod skill;
mod system_permission;
mod template;
mod vector;
mod window_state;
@ -40,6 +41,7 @@ pub use profile::{FsEmbedderProfileStore, FsProfileStore};
pub use project::FsProjectStore;
pub use secrets::FsSecretStore;
pub use skill::FsSkillStore;
pub use system_permission::FsSystemPermissionStore;
pub use template::FsTemplateStore;
pub use vector::{should_use_vector, AdaptiveMemoryRecall, VectorMemoryRecall};
pub use window_state::FsWindowStateStore;

View 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()))
}
}

View File

@ -39,21 +39,22 @@ use application::{
CreateAgentInput, CreateMemoryInput, CreateSkillInput, CreateSprintInput, DeleteAgentInput,
DeleteEmbedderProfileInput, DeleteIssueInput, DeleteMemoryInput, DeleteSkillInput,
DeleteSprintInput, DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput,
GetProjectWorkStateInput, GitBranchesInput, GitCheckoutInput, GitCommitInput, GitGraphInput,
GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput, InspectConversationInput,
LaunchAgentInput, LinkIssuesInput, ListAgentsInput, ListDevicesInput, ListIssuesInput,
ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, ListSprintsInput, LiveSessions,
McpRuntime, OpenProjectInput, PairAttemptDecision, PairDeviceInput, RateLimitKey,
ReadAgentContextInput, ReadConversationPageInput, ReadIssueCarnetInput, ReadIssueInput,
ReadMcpToolPermissionsInput, ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput,
RenameDeviceInput, RenameSprintInput, ReorderSprintsInput, ResizeTerminalInput,
ResolveAgentPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
GetProjectSystemPermissionsInput, GetProjectWorkStateInput, GitBranchesInput, GitCheckoutInput,
GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput,
InspectConversationInput, LaunchAgentInput, LinkIssuesInput, ListAgentsInput, ListDevicesInput,
ListIssuesInput, ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput,
ListSprintsInput, LiveSessions, McpRuntime, OpenProjectInput, PairAttemptDecision,
PairDeviceInput, RateLimitKey, ReadAgentContextInput, ReadConversationPageInput,
ReadIssueCarnetInput, ReadIssueInput, ReadMcpToolPermissionsInput, ReadMemoryIndexInput,
ReadProjectContextInput, RecallMemoryInput, RenameDeviceInput, RenameSprintInput,
ReorderSprintsInput, ResizeTerminalInput, ResolveAgentPermissionsInput,
ResolveAgentSystemPermissionsInput, ResolveMemoryLinksInput, RevokeDeviceInput,
RotateConversationLogInput, StopLiveAgentInput, SyncAgentWithTemplateInput, TouchDeviceInput,
UnassignSkillFromAgentInput, UnassignTicketFromSprintInput, UnlinkIssuesInput,
UpdateAgentContextInput, UpdateAgentMcpToolPermissionsInput, UpdateAgentPermissionsInput,
UpdateIssueCarnetInput, UpdateMemoryInput, UpdateProjectContextInput,
UpdateProjectMcpToolPermissionsInput, UpdateProjectPermissionsInput, UpdateSkillInput,
WriteToTerminalInput,
UpdateAgentSystemPermissionsInput, UpdateIssueCarnetInput, UpdateMemoryInput,
UpdateProjectContextInput, UpdateProjectMcpToolPermissionsInput, UpdateProjectPermissionsInput,
UpdateProjectSystemPermissionsInput, UpdateSkillInput, WriteToTerminalInput,
};
use domain::ports::PtyHandle;
use domain::IssueActor;
@ -78,22 +79,23 @@ use backend::dto::{
GraphCommitListDto, HealthRequestDto, HealthResponseDto, InspectConversationRequestDto,
LaunchAgentRequestDto, LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto,
MemoryListDto, OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectWorkStateDto,
ReadAgentContextResponseDto, ReadConversationPageRequestDto, RecallMemoryRequestDto,
ResolveAgentPermissionsRequestDto, ResumableAgentListDto, SaveEmbedderProfileRequestDto,
SaveProfileRequestDto, SkillDto, SkillListDto, SprintCreateRequestDto, SprintDeleteRequestDto,
SprintDto, SprintListDto, SprintListRequestDto, SprintRenameRequestDto,
SprintReorderRequestDto, StopLiveAgentRequestDto, StopLiveAgentResponseDto,
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
TerminalSessionDto, TicketAssignRequestDto, TicketCarnetDto, TicketCreateRequestDto,
TicketDeleteRequestDto, TicketDto, TicketLinkCommandRequestDto, TicketListPageInput,
TicketListRequestDto, TicketReadRequestDto, TicketSprintAssignRequestDto,
ProjectMcpToolPermissionsDto, ProjectPermissionsDto, ProjectSystemPermissionsDto,
ProjectWorkStateDto, ReadAgentContextResponseDto, ReadConversationPageRequestDto,
RecallMemoryRequestDto, ResolveAgentPermissionsRequestDto,
ResolveAgentSystemPermissionsRequestDto, ResolvedAgentSystemPermissionsDto,
ResumableAgentListDto, SaveEmbedderProfileRequestDto, SaveProfileRequestDto, SkillDto,
SkillListDto, SprintCreateRequestDto, SprintDeleteRequestDto, SprintDto, SprintListDto,
SprintListRequestDto, SprintRenameRequestDto, SprintReorderRequestDto, StopLiveAgentRequestDto,
StopLiveAgentResponseDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto,
TemplateListDto, TerminalSessionDto, TicketAssignRequestDto, TicketCarnetDto,
TicketCreateRequestDto, TicketDeleteRequestDto, TicketDto, TicketLinkCommandRequestDto,
TicketListPageInput, TicketListRequestDto, TicketReadRequestDto, TicketSprintAssignRequestDto,
TicketSprintUnassignRequestDto, TicketUnlinkCommandRequestDto, TicketUpdateCarnetRequestDto,
TicketUpdateRequestDto, TurnPageDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
UpdateAgentMcpToolPermissionsRequestDto, UpdateAgentPermissionsRequestDto,
UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
UpdateAgentSystemPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
UpdateProjectMcpToolPermissionsRequestDto, UpdateProjectPermissionsRequestDto,
UpdateSkillRequestDto, UpdateTemplateRequestDto,
UpdateProjectSystemPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
};
use backend::events::DomainEventDto;
type PtyChunk = Vec<u8>;
@ -2396,6 +2398,18 @@ async fn invoke(
"resolve_agent_permissions" => {
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" => {
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)
}
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(
args: &Value,
state: &BackendCore,
@ -7617,6 +7713,10 @@ mod tests {
"update_project_permissions",
"update_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",
"update_project_mcp_tool_permissions",
"update_agent_mcp_tool_permissions",