merge feature/ticket96-effective-permissions-assistants-ticket dans develop
This commit is contained in:
@ -9,8 +9,9 @@ use domain::ports::{
|
|||||||
use domain::profile::StructuredAdapter;
|
use domain::profile::StructuredAdapter;
|
||||||
use domain::AgentProfile;
|
use domain::AgentProfile;
|
||||||
use domain::{
|
use domain::{
|
||||||
AgentToolPolicy, AgentToolPolicyStore, AssistantContextProvider, DomainEvent, EventBus,
|
AgentToolPolicy, AgentToolPolicyStore, AssistantContextProvider, DomainEvent,
|
||||||
IssueRef, IssueStore, NetworkPolicy, ProfileId, ProfileStore, Project, SessionId,
|
EffectivePermissions, EventBus, IssueRef, IssueStore, NetworkPolicy, PermissionStore,
|
||||||
|
ProfileId, ProfileStore, Project, SessionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::terminal::StructuredSessions;
|
use crate::terminal::StructuredSessions;
|
||||||
@ -26,6 +27,7 @@ pub struct OpenTicketAssistant {
|
|||||||
structured: Arc<StructuredSessions>,
|
structured: Arc<StructuredSessions>,
|
||||||
policies: Arc<dyn AgentToolPolicyStore>,
|
policies: Arc<dyn AgentToolPolicyStore>,
|
||||||
events: Arc<dyn EventBus>,
|
events: Arc<dyn EventBus>,
|
||||||
|
permissions: Option<Arc<dyn PermissionStore>>,
|
||||||
system_permissions: Option<Arc<dyn SystemPermissionStore>>,
|
system_permissions: Option<Arc<dyn SystemPermissionStore>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,10 +75,21 @@ impl OpenTicketAssistant {
|
|||||||
structured,
|
structured,
|
||||||
policies,
|
policies,
|
||||||
events,
|
events,
|
||||||
|
permissions: None,
|
||||||
system_permissions: None,
|
system_permissions: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wires the project agent permission store used for assistant launches.
|
||||||
|
///
|
||||||
|
/// Ticket assistants are not agents, so only project defaults are resolved;
|
||||||
|
/// agent overrides are deliberately ignored.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_permission_store(mut self, store: Arc<dyn PermissionStore>) -> Self {
|
||||||
|
self.permissions = Some(store);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Wires the project system permission store used for assistant launches.
|
/// Wires the project system permission store used for assistant launches.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_system_permission_store(mut self, store: Arc<dyn SystemPermissionStore>) -> Self {
|
pub fn with_system_permission_store(mut self, store: Arc<dyn SystemPermissionStore>) -> Self {
|
||||||
@ -115,6 +128,9 @@ impl OpenTicketAssistant {
|
|||||||
.structured
|
.structured
|
||||||
.ticket_assistant_requester(input.issue_ref)
|
.ticket_assistant_requester(input.issue_ref)
|
||||||
.is_some();
|
.is_some();
|
||||||
|
let permissions = self
|
||||||
|
.resolve_project_default_permissions(&input.project)
|
||||||
|
.await?;
|
||||||
let policy = AgentToolPolicy::new(
|
let policy = AgentToolPolicy::new(
|
||||||
vec![
|
vec![
|
||||||
"idea_ticket_read".to_owned(),
|
"idea_ticket_read".to_owned(),
|
||||||
@ -138,6 +154,7 @@ impl OpenTicketAssistant {
|
|||||||
&profile,
|
&profile,
|
||||||
&prepared,
|
&prepared,
|
||||||
&requester,
|
&requester,
|
||||||
|
permissions.as_ref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@ -227,6 +244,20 @@ impl OpenTicketAssistant {
|
|||||||
let doc = store.load_system_permissions(project).await?;
|
let doc = store.load_system_permissions(project).await?;
|
||||||
Ok(doc.project_default.and_then(|set| set.network))
|
Ok(doc.project_default.and_then(|set| set.network))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn resolve_project_default_permissions(
|
||||||
|
&self,
|
||||||
|
project: &Project,
|
||||||
|
) -> Result<Option<EffectivePermissions>, AppError> {
|
||||||
|
let Some(store) = &self.permissions else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let doc = store.load_permissions(project).await?;
|
||||||
|
Ok(domain::permission::resolve(
|
||||||
|
doc.project_defaults.as_ref(),
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn codex_network_access(network: Option<NetworkPolicy>) -> bool {
|
fn codex_network_access(network: Option<NetworkPolicy>) -> bool {
|
||||||
|
|||||||
@ -7,19 +7,21 @@ use application::{
|
|||||||
StructuredSessions,
|
StructuredSessions,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use domain::permission::{EffectivePermissions, Posture};
|
||||||
use domain::ports::{
|
use domain::ports::{
|
||||||
AgentSession, AgentSessionError, AgentSessionFactory, ReplyStream, RuntimeError, SessionPlan,
|
AgentSession, AgentSessionError, AgentSessionFactory, PermissionStore, ReplyStream,
|
||||||
StoreError, StructuredProviderLaunchPolicy, StructuredSessionEnvironment,
|
RuntimeError, SessionPlan, StoreError, StructuredProviderLaunchPolicy,
|
||||||
StructuredSessionEnvironmentPreparer, SystemPermissionStore,
|
StructuredSessionEnvironment, StructuredSessionEnvironmentPreparer, SystemPermissionStore,
|
||||||
};
|
};
|
||||||
use domain::profile::StructuredAdapter;
|
use domain::profile::StructuredAdapter;
|
||||||
use domain::{
|
use domain::{
|
||||||
AgentProfile, AgentToolPolicy, AgentToolPolicyStore, AssistantContextError,
|
AgentId, AgentPermissionOverride, AgentProfile, AgentToolPolicy, AgentToolPolicyStore,
|
||||||
AssistantContextProvider, ContextInjection, DomainEvent, EventBus, EventStream, Issue,
|
AssistantContextError, AssistantContextProvider, ContextInjection, DomainEvent, EventBus,
|
||||||
IssueActor, IssueCarnet, IssueId, IssueListFilter, IssueNumber, IssuePriority, IssueRef,
|
EventStream, Issue, IssueActor, IssueCarnet, IssueId, IssueListFilter, IssueNumber,
|
||||||
IssueStatus, IssueStore, IssueStoreError, IssueVersion, MarkdownDoc, NetworkPolicy,
|
IssuePriority, IssueRef, IssueStatus, IssueStore, IssueStoreError, IssueVersion, MarkdownDoc,
|
||||||
PreparedContext, ProfileId, ProfileStore, Project, ProjectId, ProjectPath,
|
NetworkPolicy, PermissionSet, PreparedContext, ProfileId, ProfileStore, Project, ProjectId,
|
||||||
ProjectSystemPermissions, RemoteRef, SessionId, SystemPermissionSet,
|
ProjectPath, ProjectPermissions, ProjectSystemPermissions, RemoteRef, SessionId,
|
||||||
|
SystemPermissionSet,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -194,7 +196,14 @@ impl AssistantContextProvider for FakeAssistantContext {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FakeEnvironmentPreparer {
|
struct FakeEnvironmentPreparer {
|
||||||
calls: Mutex<Vec<(IssueRef, String, PreparedContext)>>,
|
calls: Mutex<
|
||||||
|
Vec<(
|
||||||
|
IssueRef,
|
||||||
|
String,
|
||||||
|
PreparedContext,
|
||||||
|
Option<EffectivePermissions>,
|
||||||
|
)>,
|
||||||
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@ -206,11 +215,14 @@ impl StructuredSessionEnvironmentPreparer for FakeEnvironmentPreparer {
|
|||||||
_profile: &AgentProfile,
|
_profile: &AgentProfile,
|
||||||
prepared: &PreparedContext,
|
prepared: &PreparedContext,
|
||||||
requester: &str,
|
requester: &str,
|
||||||
|
permissions: Option<&EffectivePermissions>,
|
||||||
) -> Result<StructuredSessionEnvironment, RuntimeError> {
|
) -> Result<StructuredSessionEnvironment, RuntimeError> {
|
||||||
self.calls
|
self.calls.lock().unwrap().push((
|
||||||
.lock()
|
issue_ref,
|
||||||
.unwrap()
|
requester.to_owned(),
|
||||||
.push((issue_ref, requester.to_owned(), prepared.clone()));
|
prepared.clone(),
|
||||||
|
permissions.cloned(),
|
||||||
|
));
|
||||||
Ok(StructuredSessionEnvironment {
|
Ok(StructuredSessionEnvironment {
|
||||||
cwd: ProjectPath::new("/tmp/app-data/assistant/tickets/1/7").unwrap(),
|
cwd: ProjectPath::new("/tmp/app-data/assistant/tickets/1/7").unwrap(),
|
||||||
env: vec![(
|
env: vec![(
|
||||||
@ -346,6 +358,27 @@ impl SystemPermissionStore for FakeSystemPermissionStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FakePermissionStore(ProjectPermissions);
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PermissionStore for FakePermissionStore {
|
||||||
|
async fn load_permissions(&self, _project: &Project) -> Result<ProjectPermissions, StoreError> {
|
||||||
|
Ok(self.0.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_permissions(
|
||||||
|
&self,
|
||||||
|
_project: &Project,
|
||||||
|
_permissions: &ProjectPermissions,
|
||||||
|
) -> Result<(), StoreError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn permission_doc(project_defaults: Option<PermissionSet>) -> ProjectPermissions {
|
||||||
|
ProjectPermissions::new(project_defaults, Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn open_then_close_ticket_assistant_sets_policy_injects_context_and_emits_events() {
|
async fn open_then_close_ticket_assistant_sets_policy_injects_context_and_emits_events() {
|
||||||
let issue = issue(7);
|
let issue = issue(7);
|
||||||
@ -406,6 +439,7 @@ async fn open_then_close_ticket_assistant_sets_policy_injects_context_and_emits_
|
|||||||
assert_eq!(environment_calls.len(), 1);
|
assert_eq!(environment_calls.len(), 1);
|
||||||
assert_eq!(environment_calls[0].0, issue.reference());
|
assert_eq!(environment_calls[0].0, issue.reference());
|
||||||
assert_eq!(environment_calls[0].1, output.requester);
|
assert_eq!(environment_calls[0].1, output.requester);
|
||||||
|
assert!(environment_calls[0].3.is_none());
|
||||||
drop(environment_calls);
|
drop(environment_calls);
|
||||||
let starts = factory.starts.lock().unwrap();
|
let starts = factory.starts.lock().unwrap();
|
||||||
assert_eq!(starts[0].1.as_str(), "/tmp/app-data/assistant/tickets/1/7");
|
assert_eq!(starts[0].1.as_str(), "/tmp/app-data/assistant/tickets/1/7");
|
||||||
@ -442,6 +476,120 @@ async fn open_then_close_ticket_assistant_sets_policy_injects_context_and_emits_
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ticket_assistant_without_project_defaults_keeps_native_permissions() {
|
||||||
|
let issue = issue(7);
|
||||||
|
let profile_id = ProfileId::from_uuid(Uuid::from_u128(9));
|
||||||
|
let environment = Arc::new(FakeEnvironmentPreparer::default());
|
||||||
|
let open = OpenTicketAssistant::new(
|
||||||
|
Arc::new(FakeIssues {
|
||||||
|
issue: issue.clone(),
|
||||||
|
}),
|
||||||
|
Arc::new(FakeProfiles {
|
||||||
|
profile: profile(profile_id),
|
||||||
|
}),
|
||||||
|
Arc::new(FakeAssistantContext::default()),
|
||||||
|
environment.clone(),
|
||||||
|
Arc::new(FakeFactory::default()),
|
||||||
|
Arc::new(StructuredSessions::new()),
|
||||||
|
Arc::new(FakePolicies::default()),
|
||||||
|
Arc::new(SpyBus::default()),
|
||||||
|
)
|
||||||
|
.with_permission_store(Arc::new(FakePermissionStore(permission_doc(None))));
|
||||||
|
|
||||||
|
open.execute(OpenTicketAssistantInput {
|
||||||
|
project: project(),
|
||||||
|
issue_ref: issue.reference(),
|
||||||
|
profile_id,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let calls = environment.calls.lock().unwrap();
|
||||||
|
assert!(calls[0].3.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ticket_assistant_projects_project_default_permissions() {
|
||||||
|
let issue = issue(7);
|
||||||
|
let profile_id = ProfileId::from_uuid(Uuid::from_u128(9));
|
||||||
|
let environment = Arc::new(FakeEnvironmentPreparer::default());
|
||||||
|
let open = OpenTicketAssistant::new(
|
||||||
|
Arc::new(FakeIssues {
|
||||||
|
issue: issue.clone(),
|
||||||
|
}),
|
||||||
|
Arc::new(FakeProfiles {
|
||||||
|
profile: profile(profile_id),
|
||||||
|
}),
|
||||||
|
Arc::new(FakeAssistantContext::default()),
|
||||||
|
environment.clone(),
|
||||||
|
Arc::new(FakeFactory::default()),
|
||||||
|
Arc::new(StructuredSessions::new()),
|
||||||
|
Arc::new(FakePolicies::default()),
|
||||||
|
Arc::new(SpyBus::default()),
|
||||||
|
)
|
||||||
|
.with_permission_store(Arc::new(FakePermissionStore(permission_doc(Some(
|
||||||
|
PermissionSet::new(Vec::new(), Posture::Allow),
|
||||||
|
)))));
|
||||||
|
|
||||||
|
open.execute(OpenTicketAssistantInput {
|
||||||
|
project: project(),
|
||||||
|
issue_ref: issue.reference(),
|
||||||
|
profile_id,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let calls = environment.calls.lock().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
calls[0].3.as_ref().map(EffectivePermissions::fallback),
|
||||||
|
Some(Posture::Allow)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ticket_assistant_ignores_agent_permission_overrides() {
|
||||||
|
let issue = issue(7);
|
||||||
|
let profile_id = ProfileId::from_uuid(Uuid::from_u128(9));
|
||||||
|
let environment = Arc::new(FakeEnvironmentPreparer::default());
|
||||||
|
let doc = ProjectPermissions::new(
|
||||||
|
Some(PermissionSet::new(Vec::new(), Posture::Allow)),
|
||||||
|
vec![AgentPermissionOverride::new(
|
||||||
|
AgentId::from_uuid(Uuid::from_u128(42)),
|
||||||
|
PermissionSet::new(Vec::new(), Posture::Deny),
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
let open = OpenTicketAssistant::new(
|
||||||
|
Arc::new(FakeIssues {
|
||||||
|
issue: issue.clone(),
|
||||||
|
}),
|
||||||
|
Arc::new(FakeProfiles {
|
||||||
|
profile: profile(profile_id),
|
||||||
|
}),
|
||||||
|
Arc::new(FakeAssistantContext::default()),
|
||||||
|
environment.clone(),
|
||||||
|
Arc::new(FakeFactory::default()),
|
||||||
|
Arc::new(StructuredSessions::new()),
|
||||||
|
Arc::new(FakePolicies::default()),
|
||||||
|
Arc::new(SpyBus::default()),
|
||||||
|
)
|
||||||
|
.with_permission_store(Arc::new(FakePermissionStore(doc)));
|
||||||
|
|
||||||
|
open.execute(OpenTicketAssistantInput {
|
||||||
|
project: project(),
|
||||||
|
issue_ref: issue.reference(),
|
||||||
|
profile_id,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let calls = environment.calls.lock().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
calls[0].3.as_ref().map(EffectivePermissions::fallback),
|
||||||
|
Some(Posture::Allow)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn codex_ticket_assistant_uses_project_network_permission_for_structured_policy() {
|
async fn codex_ticket_assistant_uses_project_network_permission_for_structured_policy() {
|
||||||
for (policy, network_access) in [
|
for (policy, network_access) in [
|
||||||
|
|||||||
@ -1651,6 +1651,8 @@ impl BackendCore {
|
|||||||
let system_permission_store = Arc::new(FsSystemPermissionStore::new(Arc::clone(&fs_port)));
|
let system_permission_store = Arc::new(FsSystemPermissionStore::new(Arc::clone(&fs_port)));
|
||||||
let system_permission_store_port =
|
let system_permission_store_port =
|
||||||
Arc::clone(&system_permission_store) as Arc<dyn SystemPermissionStore>;
|
Arc::clone(&system_permission_store) as Arc<dyn SystemPermissionStore>;
|
||||||
|
let permission_store = Arc::new(FsPermissionStore::new(Arc::clone(&fs_port)));
|
||||||
|
let permission_store_port = Arc::clone(&permission_store) as Arc<dyn PermissionStore>;
|
||||||
let assistant_context_provider = Arc::new(FsAssistantContextStore::new(
|
let assistant_context_provider = Arc::new(FsAssistantContextStore::new(
|
||||||
Arc::clone(&fs_port),
|
Arc::clone(&fs_port),
|
||||||
app_data_dir.to_string_lossy().into_owned(),
|
app_data_dir.to_string_lossy().into_owned(),
|
||||||
@ -1682,6 +1684,7 @@ impl BackendCore {
|
|||||||
Arc::clone(&tool_policy_store),
|
Arc::clone(&tool_policy_store),
|
||||||
Arc::clone(&events_port),
|
Arc::clone(&events_port),
|
||||||
)
|
)
|
||||||
|
.with_permission_store(Arc::clone(&permission_store_port))
|
||||||
.with_system_permission_store(Arc::clone(&system_permission_store_port)),
|
.with_system_permission_store(Arc::clone(&system_permission_store_port)),
|
||||||
);
|
);
|
||||||
let close_ticket_assistant = Arc::new(CloseTicketAssistant::new(
|
let close_ticket_assistant = Arc::new(CloseTicketAssistant::new(
|
||||||
@ -1691,8 +1694,6 @@ impl BackendCore {
|
|||||||
));
|
));
|
||||||
|
|
||||||
// --- Project permissions (LP1) ---
|
// --- 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 runtime_permission_probe =
|
let runtime_permission_probe =
|
||||||
Arc::new(ReadOnlyRuntimePermissionProbe) as Arc<dyn RuntimePermissionProbe>;
|
Arc::new(ReadOnlyRuntimePermissionProbe) as Arc<dyn RuntimePermissionProbe>;
|
||||||
let get_project_system_permissions = Arc::new(GetProjectSystemPermissions::new(
|
let get_project_system_permissions = Arc::new(GetProjectSystemPermissions::new(
|
||||||
|
|||||||
@ -49,6 +49,7 @@ use crate::model_catalogue::{CliVersion, CompatibilityMatrix};
|
|||||||
use crate::model_server::{
|
use crate::model_server::{
|
||||||
HfModelRef, LocalModelServerConfig, ModelPath, ModelServerEndpoint, ModelServerStatus,
|
HfModelRef, LocalModelServerConfig, ModelPath, ModelServerEndpoint, ModelServerStatus,
|
||||||
};
|
};
|
||||||
|
use crate::permission::EffectivePermissions;
|
||||||
use crate::permission::ProjectPermissions;
|
use crate::permission::ProjectPermissions;
|
||||||
use crate::plugin::{
|
use crate::plugin::{
|
||||||
ContentHash, PluginBundleUrl, PluginId, PluginManifest, PluginMcpServerSpec,
|
ContentHash, PluginBundleUrl, PluginId, PluginManifest, PluginMcpServerSpec,
|
||||||
@ -210,6 +211,7 @@ pub trait StructuredSessionEnvironmentPreparer: Send + Sync {
|
|||||||
profile: &AgentProfile,
|
profile: &AgentProfile,
|
||||||
prepared: &PreparedContext,
|
prepared: &PreparedContext,
|
||||||
requester: &str,
|
requester: &str,
|
||||||
|
permissions: Option<&EffectivePermissions>,
|
||||||
) -> Result<StructuredSessionEnvironment, RuntimeError>;
|
) -> Result<StructuredSessionEnvironment, RuntimeError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -214,6 +214,7 @@ impl TicketAssistantEnvironmentPreparer {
|
|||||||
cwd: &ProjectPath,
|
cwd: &ProjectPath,
|
||||||
requester: &str,
|
requester: &str,
|
||||||
env: &mut Vec<(String, String)>,
|
env: &mut Vec<(String, String)>,
|
||||||
|
eff: Option<&EffectivePermissions>,
|
||||||
) -> Result<(), RuntimeError> {
|
) -> Result<(), RuntimeError> {
|
||||||
let Some(mcp) = &profile.mcp else {
|
let Some(mcp) = &profile.mcp else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@ -244,14 +245,8 @@ impl TicketAssistantEnvironmentPreparer {
|
|||||||
if profile.structured_adapter != Some(StructuredAdapter::OpenCode) {
|
if profile.structured_adapter != Some(StructuredAdapter::OpenCode) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
// No `PermissionStore` is wired for ticket-assistant sessions (no
|
|
||||||
// per-agent policy exists in this path, for any CLI) — `eff` is
|
|
||||||
// always `None` here, which per `opencode_permission_block`'s
|
|
||||||
// contract omits the `permission` key entirely and preserves
|
|
||||||
// OpenCode's native prompting, matching Claude/Codex's behaviour
|
|
||||||
// on this same path.
|
|
||||||
let body = if let Some(opencode) = profile.opencode.as_ref() {
|
let body = if let Some(opencode) = profile.opencode.as_ref() {
|
||||||
opencode_config_json(opencode, project.root.as_str(), runtime.as_ref(), None)
|
opencode_config_json(opencode, project.root.as_str(), runtime.as_ref(), eff)
|
||||||
.to_string()
|
.to_string()
|
||||||
} else if let Some(provider) = profile.opencode_provider.as_ref() {
|
} else if let Some(provider) = profile.opencode_provider.as_ref() {
|
||||||
let api_key = self.resolve_opencode_provider_api_key(provider).await?;
|
let api_key = self.resolve_opencode_provider_api_key(provider).await?;
|
||||||
@ -260,7 +255,7 @@ impl TicketAssistantEnvironmentPreparer {
|
|||||||
&api_key,
|
&api_key,
|
||||||
project.root.as_str(),
|
project.root.as_str(),
|
||||||
runtime.as_ref(),
|
runtime.as_ref(),
|
||||||
None,
|
eff,
|
||||||
)
|
)
|
||||||
.to_string()
|
.to_string()
|
||||||
} else {
|
} else {
|
||||||
@ -304,6 +299,7 @@ impl StructuredSessionEnvironmentPreparer for TicketAssistantEnvironmentPreparer
|
|||||||
profile: &AgentProfile,
|
profile: &AgentProfile,
|
||||||
prepared: &PreparedContext,
|
prepared: &PreparedContext,
|
||||||
requester: &str,
|
requester: &str,
|
||||||
|
permissions: Option<&EffectivePermissions>,
|
||||||
) -> Result<StructuredSessionEnvironment, RuntimeError> {
|
) -> Result<StructuredSessionEnvironment, RuntimeError> {
|
||||||
let run_dir = self.run_dir(project, issue_ref)?;
|
let run_dir = self.run_dir(project, issue_ref)?;
|
||||||
self.create_dir(run_dir.as_str()).await?;
|
self.create_dir(run_dir.as_str()).await?;
|
||||||
@ -313,8 +309,15 @@ impl StructuredSessionEnvironmentPreparer for TicketAssistantEnvironmentPreparer
|
|||||||
let mut env = spec.env;
|
let mut env = spec.env;
|
||||||
self.materialise_context(spec.context_plan, &spec.cwd, prepared, &mut env)
|
self.materialise_context(spec.context_plan, &spec.cwd, prepared, &mut env)
|
||||||
.await?;
|
.await?;
|
||||||
self.materialise_mcp(project, profile, &spec.cwd, requester, &mut env)
|
self.materialise_mcp(
|
||||||
.await?;
|
project,
|
||||||
|
profile,
|
||||||
|
&spec.cwd,
|
||||||
|
requester,
|
||||||
|
&mut env,
|
||||||
|
permissions,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(StructuredSessionEnvironment {
|
Ok(StructuredSessionEnvironment {
|
||||||
cwd: spec.cwd,
|
cwd: spec.cwd,
|
||||||
env,
|
env,
|
||||||
|
|||||||
@ -3,8 +3,11 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use application::McpRuntime;
|
use application::McpRuntime;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use domain::permission::{EffectivePermissions, PermissionSet, Posture};
|
||||||
use domain::ports::{SessionPlan, StructuredSessionEnvironmentPreparer};
|
use domain::ports::{SessionPlan, StructuredSessionEnvironmentPreparer};
|
||||||
use domain::profile::{McpCapability, McpConfigStrategy, McpTransport};
|
use domain::profile::{
|
||||||
|
McpCapability, McpConfigStrategy, McpTransport, OpenCodeConfig, StructuredAdapter,
|
||||||
|
};
|
||||||
use domain::{
|
use domain::{
|
||||||
AgentProfile, AgentRuntime, AssistantContextProvider, ContextInjection, ContextInjectionPlan,
|
AgentProfile, AgentRuntime, AssistantContextProvider, ContextInjection, ContextInjectionPlan,
|
||||||
FileSystem, Issue, IssueActor, IssueId, IssueNumber, IssuePriority, IssueStatus, MarkdownDoc,
|
FileSystem, Issue, IssueActor, IssueId, IssueNumber, IssuePriority, IssueStatus, MarkdownDoc,
|
||||||
@ -122,6 +125,32 @@ fn mcp_profile() -> AgentProfile {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn opencode_profile() -> AgentProfile {
|
||||||
|
AgentProfile::new(
|
||||||
|
ProfileId::from_uuid(Uuid::from_u128(10)),
|
||||||
|
"OpenCode",
|
||||||
|
"opencode",
|
||||||
|
Vec::new(),
|
||||||
|
ContextInjection::stdin(),
|
||||||
|
None,
|
||||||
|
"{projectRoot}",
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.with_structured_adapter(StructuredAdapter::OpenCode)
|
||||||
|
.with_opencode(
|
||||||
|
OpenCodeConfig::new("http://localhost:8080/v1", None, "qwen3-coder", None, None).unwrap(),
|
||||||
|
)
|
||||||
|
.with_mcp(McpCapability::new(
|
||||||
|
McpConfigStrategy::open_code_config("opencode.json").unwrap(),
|
||||||
|
McpTransport::Stdio,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective(fallback: Posture) -> EffectivePermissions {
|
||||||
|
domain::permission::resolve(Some(&PermissionSet::new(Vec::new(), fallback)), None).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn default_context_is_embedded_and_injects_the_ticket() {
|
async fn default_context_is_embedded_and_injects_the_ticket() {
|
||||||
let tmp = TempDir::new();
|
let tmp = TempDir::new();
|
||||||
@ -205,6 +234,7 @@ async fn environment_preparer_materialises_context_and_mcp_under_isolated_app_da
|
|||||||
&mcp_profile(),
|
&mcp_profile(),
|
||||||
&prepared,
|
&prepared,
|
||||||
"ticket-assistant:00000000000000000000000000000001:7",
|
"ticket-assistant:00000000000000000000000000000001:7",
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@ -246,3 +276,102 @@ async fn environment_preparer_materialises_context_and_mcp_under_isolated_app_da
|
|||||||
"assistant context must not be written into the real project ticket carnet"
|
"assistant context must not be written into the real project ticket carnet"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn opencode_ticket_assistant_projects_project_permissions_into_isolated_config() {
|
||||||
|
let tmp = TempDir::new();
|
||||||
|
let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
|
||||||
|
let app_data_dir = tmp.app_data_dir();
|
||||||
|
let project = project(tmp.project_root());
|
||||||
|
let prepared = PreparedContext {
|
||||||
|
content: MarkdownDoc::new("assistant-only context"),
|
||||||
|
relative_path: "ticket-assistant.md".to_owned(),
|
||||||
|
project_root: project.root.as_str().to_owned(),
|
||||||
|
};
|
||||||
|
let requester = "ticket-assistant:00000000000000000000000000000001:7".to_owned();
|
||||||
|
let preparer = TicketAssistantEnvironmentPreparer::new(
|
||||||
|
fs.clone(),
|
||||||
|
app_data_dir,
|
||||||
|
Arc::new(FakeRuntime),
|
||||||
|
Arc::new(move |_, _| {
|
||||||
|
Some(McpRuntime {
|
||||||
|
exe: "/opt/idea/idea".to_owned(),
|
||||||
|
endpoint: "127.0.0.1:4567".to_owned(),
|
||||||
|
project_id: "00000000000000000000000000000001".to_owned(),
|
||||||
|
requester: requester.clone(),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
Arc::new(FsSecretStore::new(fs.clone(), tmp.app_data_dir())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let env = preparer
|
||||||
|
.prepare_ticket_assistant(
|
||||||
|
&project,
|
||||||
|
issue(7).reference(),
|
||||||
|
&opencode_profile(),
|
||||||
|
&prepared,
|
||||||
|
"ticket-assistant:00000000000000000000000000000001:7",
|
||||||
|
Some(&effective(Posture::Deny)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config_path = PathBuf::from(env.cwd.as_str()).join("opencode.json");
|
||||||
|
let config: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs.read(&RemotePath::new(config_path.to_string_lossy().into_owned()))
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(config["permission"]["bash"], "deny");
|
||||||
|
assert_eq!(config["permission"]["edit"], "deny");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn claude_ticket_assistant_does_not_project_effective_permissions() {
|
||||||
|
let tmp = TempDir::new();
|
||||||
|
let fs: Arc<dyn FileSystem> = Arc::new(LocalFileSystem::new());
|
||||||
|
let app_data_dir = tmp.app_data_dir();
|
||||||
|
let project = project(tmp.project_root());
|
||||||
|
let prepared = PreparedContext {
|
||||||
|
content: MarkdownDoc::new("assistant-only context"),
|
||||||
|
relative_path: "ticket-assistant.md".to_owned(),
|
||||||
|
project_root: project.root.as_str().to_owned(),
|
||||||
|
};
|
||||||
|
let requester = "ticket-assistant:00000000000000000000000000000001:7".to_owned();
|
||||||
|
let preparer = TicketAssistantEnvironmentPreparer::new(
|
||||||
|
fs.clone(),
|
||||||
|
app_data_dir,
|
||||||
|
Arc::new(FakeRuntime),
|
||||||
|
Arc::new(move |_, _| {
|
||||||
|
Some(McpRuntime {
|
||||||
|
exe: "/opt/idea/idea".to_owned(),
|
||||||
|
endpoint: "127.0.0.1:4567".to_owned(),
|
||||||
|
project_id: "00000000000000000000000000000001".to_owned(),
|
||||||
|
requester: requester.clone(),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
Arc::new(FsSecretStore::new(fs.clone(), tmp.app_data_dir())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let env = preparer
|
||||||
|
.prepare_ticket_assistant(
|
||||||
|
&project,
|
||||||
|
issue(7).reference(),
|
||||||
|
&mcp_profile(),
|
||||||
|
&prepared,
|
||||||
|
"ticket-assistant:00000000000000000000000000000001:7",
|
||||||
|
Some(&effective(Posture::Deny)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mcp_path = PathBuf::from(env.cwd.as_str()).join(".mcp.json");
|
||||||
|
let mcp: serde_json::Value = serde_json::from_slice(
|
||||||
|
&fs.read(&RemotePath::new(mcp_path.to_string_lossy().into_owned()))
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(mcp.get("permission").is_none());
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user