merge feature/ticket96-effective-permissions-assistants-ticket dans develop
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user