fix(backend): projeter les EffectivePermissions dans le bloc permission d'opencode.json
Les 4 générateurs opencode.json/opencode_provider.json codaient en dur
{"bash":"ask","edit":"ask"}, ignorant les permissions configurées côté
IdeA pour l'agent. Claude et Codex appliquaient déjà PermissionProjector,
seul OpenCode passait à côté.
Ajoute domain::opencode_permission_block(eff: Option<&EffectivePermissions>)
qui mappe bash ← posture bash effective, edit ← posture Write effective
(Read/Delete non exprimables dans le schéma OpenCode, déjà enforcées par
le sandbox Landlock). eff == None omet la clé permission entièrement,
préservant le prompting natif OpenCode — même invariant que Claude/Codex.
Câble eff jusqu'aux 4 sites d'appel (lifecycle.rs + assistant/mod.rs,
variantes llamacpp et provider cloud). Le chemin ticket-assistant
(assistant/mod.rs) n'a pas de PermissionStore par agent pour l'instant,
donc eff y reste None (comportement inchangé, pas de régression).
Réf ticket #94.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -8,9 +8,9 @@ use domain::ports::{SecretStore, SessionPlan};
|
||||
use domain::profile::{McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter};
|
||||
use domain::{
|
||||
AgentProfile, AgentRuntime, AssistantContextError, AssistantContextProvider,
|
||||
ContextInjectionPlan, FileSystem, FsError, Issue, IssueRef, MarkdownDoc, McpServerWiring,
|
||||
PreparedContext, Project, ProjectPath, RemotePath, RuntimeError, StructuredSessionEnvironment,
|
||||
StructuredSessionEnvironmentPreparer,
|
||||
ContextInjectionPlan, EffectivePermissions, FileSystem, FsError, Issue, IssueRef, MarkdownDoc,
|
||||
McpServerWiring, PreparedContext, Project, ProjectPath, RemotePath, RuntimeError,
|
||||
StructuredSessionEnvironment, StructuredSessionEnvironmentPreparer,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
@ -243,8 +243,14 @@ 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())
|
||||
opencode_config_json(opencode, project.root.as_str(), runtime.as_ref(), None)
|
||||
.to_string()
|
||||
} else if let Some(provider) = profile.opencode_provider.as_ref() {
|
||||
let api_key = self.resolve_opencode_provider_api_key(provider).await?;
|
||||
@ -253,6 +259,7 @@ impl TicketAssistantEnvironmentPreparer {
|
||||
&api_key,
|
||||
project.root.as_str(),
|
||||
runtime.as_ref(),
|
||||
None,
|
||||
)
|
||||
.to_string()
|
||||
} else {
|
||||
@ -337,6 +344,7 @@ fn opencode_config_json(
|
||||
config: &domain::profile::OpenCodeConfig,
|
||||
project_root: &str,
|
||||
runtime: Option<&McpRuntime>,
|
||||
eff: Option<&EffectivePermissions>,
|
||||
) -> Value {
|
||||
let model = config.model.as_str();
|
||||
let opencode_model = format!("llamacpp/{model}");
|
||||
@ -403,13 +411,9 @@ fn opencode_config_json(
|
||||
}
|
||||
}),
|
||||
);
|
||||
root.insert(
|
||||
"permission".to_owned(),
|
||||
json!({
|
||||
"bash": "ask",
|
||||
"edit": "ask"
|
||||
}),
|
||||
);
|
||||
if let Some(permission) = domain::opencode_permission_block(eff) {
|
||||
root.insert("permission".to_owned(), permission);
|
||||
}
|
||||
root.insert(
|
||||
"disabled_providers".to_owned(),
|
||||
json!(["anthropic", "openai", "gemini", "ollama"]),
|
||||
@ -426,6 +430,7 @@ fn opencode_provider_config_json(
|
||||
api_key: &str,
|
||||
project_root: &str,
|
||||
runtime: Option<&McpRuntime>,
|
||||
eff: Option<&EffectivePermissions>,
|
||||
) -> Value {
|
||||
let mut root = Map::new();
|
||||
root.insert(
|
||||
@ -491,13 +496,9 @@ fn opencode_provider_config_json(
|
||||
}
|
||||
}),
|
||||
);
|
||||
root.insert(
|
||||
"permission".to_owned(),
|
||||
json!({
|
||||
"bash": "ask",
|
||||
"edit": "ask"
|
||||
}),
|
||||
);
|
||||
if let Some(permission) = domain::opencode_permission_block(eff) {
|
||||
root.insert("permission".to_owned(), permission);
|
||||
}
|
||||
Value::Object(root)
|
||||
}
|
||||
|
||||
@ -636,7 +637,7 @@ mod opencode_provider_config_json_tests {
|
||||
#[test]
|
||||
fn known_provider_emits_only_the_api_key_option() {
|
||||
let config = known_provider_config();
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None);
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None, None);
|
||||
let provider = &body["provider"]["anthropic"];
|
||||
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
||||
assert!(provider["options"].get("baseURL").is_none());
|
||||
@ -647,7 +648,7 @@ mod opencode_provider_config_json_tests {
|
||||
#[test]
|
||||
fn custom_provider_emits_npm_base_url_and_models() {
|
||||
let config = custom_provider_config();
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None);
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None, None);
|
||||
let provider = &body["provider"]["my-custom"];
|
||||
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
||||
assert_eq!(provider["options"]["baseURL"], "https://my-endpoint/v1");
|
||||
@ -666,10 +667,88 @@ mod opencode_provider_config_json_tests {
|
||||
let config = OpenCodeProviderConfig::new("my-custom", "my-model", SecretRef::new("ref"))
|
||||
.unwrap()
|
||||
.with_custom(custom);
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None);
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None, None);
|
||||
assert_eq!(
|
||||
body["provider"]["my-custom"]["models"]["my-model"]["name"],
|
||||
"My Model"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- permission projection wiring (ticket #94) -----------------------
|
||||
|
||||
/// Builds an [`EffectivePermissions`] with the given fallback posture (only
|
||||
/// the fallback drives the blanket bash/edit verdicts checked here).
|
||||
fn eff(fallback: domain::permission::Posture) -> EffectivePermissions {
|
||||
domain::permission::resolve(
|
||||
Some(&domain::permission::PermissionSet::new(vec![], fallback)),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_config_json_omits_permission_key_when_eff_is_none() {
|
||||
let config =
|
||||
domain::profile::OpenCodeConfig::new("http://localhost:8080", None, "m", None, None)
|
||||
.unwrap();
|
||||
let body = opencode_config_json(&config, "/project", None, None);
|
||||
assert!(body.get("permission").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_config_json_projects_allow_and_deny() {
|
||||
let config =
|
||||
domain::profile::OpenCodeConfig::new("http://localhost:8080", None, "m", None, None)
|
||||
.unwrap();
|
||||
|
||||
let allowed = opencode_config_json(
|
||||
&config,
|
||||
"/project",
|
||||
None,
|
||||
Some(&eff(domain::permission::Posture::Allow)),
|
||||
);
|
||||
assert_eq!(allowed["permission"]["bash"], "allow");
|
||||
assert_eq!(allowed["permission"]["edit"], "allow");
|
||||
|
||||
let denied = opencode_config_json(
|
||||
&config,
|
||||
"/project",
|
||||
None,
|
||||
Some(&eff(domain::permission::Posture::Deny)),
|
||||
);
|
||||
assert_eq!(denied["permission"]["bash"], "deny");
|
||||
assert_eq!(denied["permission"]["edit"], "deny");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_config_json_omits_permission_key_when_eff_is_none() {
|
||||
let config = known_provider_config();
|
||||
let body = opencode_provider_config_json(&config, "sk-live", "/project", None, None);
|
||||
assert!(body.get("permission").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_config_json_projects_allow_and_deny() {
|
||||
let config = known_provider_config();
|
||||
|
||||
let allowed = opencode_provider_config_json(
|
||||
&config,
|
||||
"sk-live",
|
||||
"/project",
|
||||
None,
|
||||
Some(&eff(domain::permission::Posture::Allow)),
|
||||
);
|
||||
assert_eq!(allowed["permission"]["bash"], "allow");
|
||||
assert_eq!(allowed["permission"]["edit"], "allow");
|
||||
|
||||
let denied = opencode_provider_config_json(
|
||||
&config,
|
||||
"sk-live",
|
||||
"/project",
|
||||
None,
|
||||
Some(&eff(domain::permission::Posture::Deny)),
|
||||
);
|
||||
assert_eq!(denied["permission"]["bash"], "deny");
|
||||
assert_eq!(denied["permission"]["edit"], "deny");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user