merge feature/ticket96-effective-permissions-assistants-ticket dans develop

This commit is contained in:
2026-07-27 14:18:59 +02:00
6 changed files with 343 additions and 29 deletions

View File

@ -214,6 +214,7 @@ impl TicketAssistantEnvironmentPreparer {
cwd: &ProjectPath,
requester: &str,
env: &mut Vec<(String, String)>,
eff: Option<&EffectivePermissions>,
) -> Result<(), RuntimeError> {
let Some(mcp) = &profile.mcp else {
return Ok(());
@ -244,14 +245,8 @@ impl TicketAssistantEnvironmentPreparer {
if profile.structured_adapter != Some(StructuredAdapter::OpenCode) {
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() {
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()
} else if let Some(provider) = profile.opencode_provider.as_ref() {
let api_key = self.resolve_opencode_provider_api_key(provider).await?;
@ -260,7 +255,7 @@ impl TicketAssistantEnvironmentPreparer {
&api_key,
project.root.as_str(),
runtime.as_ref(),
None,
eff,
)
.to_string()
} else {
@ -304,6 +299,7 @@ impl StructuredSessionEnvironmentPreparer for TicketAssistantEnvironmentPreparer
profile: &AgentProfile,
prepared: &PreparedContext,
requester: &str,
permissions: Option<&EffectivePermissions>,
) -> Result<StructuredSessionEnvironment, RuntimeError> {
let run_dir = self.run_dir(project, issue_ref)?;
self.create_dir(run_dir.as_str()).await?;
@ -313,8 +309,15 @@ impl StructuredSessionEnvironmentPreparer for TicketAssistantEnvironmentPreparer
let mut env = spec.env;
self.materialise_context(spec.context_plan, &spec.cwd, prepared, &mut env)
.await?;
self.materialise_mcp(project, profile, &spec.cwd, requester, &mut env)
.await?;
self.materialise_mcp(
project,
profile,
&spec.cwd,
requester,
&mut env,
permissions,
)
.await?;
Ok(StructuredSessionEnvironment {
cwd: spec.cwd,
env,

View File

@ -3,8 +3,11 @@ use std::sync::Arc;
use application::McpRuntime;
use async_trait::async_trait;
use domain::permission::{EffectivePermissions, PermissionSet, Posture};
use domain::ports::{SessionPlan, StructuredSessionEnvironmentPreparer};
use domain::profile::{McpCapability, McpConfigStrategy, McpTransport};
use domain::profile::{
McpCapability, McpConfigStrategy, McpTransport, OpenCodeConfig, StructuredAdapter,
};
use domain::{
AgentProfile, AgentRuntime, AssistantContextProvider, ContextInjection, ContextInjectionPlan,
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]
async fn default_context_is_embedded_and_injects_the_ticket() {
let tmp = TempDir::new();
@ -205,6 +234,7 @@ async fn environment_preparer_materialises_context_and_mcp_under_isolated_app_da
&mcp_profile(),
&prepared,
"ticket-assistant:00000000000000000000000000000001:7",
None,
)
.await
.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"
);
}
#[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());
}