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:
@ -1713,6 +1713,7 @@ impl LaunchAgent {
|
|||||||
&run_dir,
|
&run_dir,
|
||||||
&input.project.root,
|
&input.project.root,
|
||||||
input.mcp_runtime.as_ref(),
|
input.mcp_runtime.as_ref(),
|
||||||
|
effective_permissions.as_ref(),
|
||||||
&mut spec,
|
&mut spec,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@ -2271,6 +2272,7 @@ impl LaunchAgent {
|
|||||||
run_dir: &ProjectPath,
|
run_dir: &ProjectPath,
|
||||||
project_root: &ProjectPath,
|
project_root: &ProjectPath,
|
||||||
runtime: Option<&McpRuntime>,
|
runtime: Option<&McpRuntime>,
|
||||||
|
eff: Option<&EffectivePermissions>,
|
||||||
spec: &mut SpawnSpec,
|
spec: &mut SpawnSpec,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let Some(mcp) = &profile.mcp else {
|
let Some(mcp) = &profile.mcp else {
|
||||||
@ -2363,7 +2365,7 @@ impl LaunchAgent {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
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).to_string()
|
opencode_config_json(opencode, project_root.as_str(), runtime, eff).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?;
|
||||||
opencode_provider_config_json(
|
opencode_provider_config_json(
|
||||||
@ -2371,6 +2373,7 @@ impl LaunchAgent {
|
|||||||
&api_key,
|
&api_key,
|
||||||
project_root.as_str(),
|
project_root.as_str(),
|
||||||
runtime,
|
runtime,
|
||||||
|
eff,
|
||||||
)
|
)
|
||||||
.to_string()
|
.to_string()
|
||||||
} else {
|
} else {
|
||||||
@ -2646,6 +2649,7 @@ fn opencode_config_json(
|
|||||||
config: &domain::profile::OpenCodeConfig,
|
config: &domain::profile::OpenCodeConfig,
|
||||||
project_root: &str,
|
project_root: &str,
|
||||||
runtime: Option<&McpRuntime>,
|
runtime: Option<&McpRuntime>,
|
||||||
|
eff: Option<&EffectivePermissions>,
|
||||||
) -> serde_json::Value {
|
) -> serde_json::Value {
|
||||||
let model = config.model.as_str();
|
let model = config.model.as_str();
|
||||||
let opencode_model = format!("llamacpp/{model}");
|
let opencode_model = format!("llamacpp/{model}");
|
||||||
@ -2725,13 +2729,9 @@ fn opencode_config_json(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
root.insert(
|
if let Some(permission) = domain::opencode_permission_block(eff) {
|
||||||
"permission".to_owned(),
|
root.insert("permission".to_owned(), permission);
|
||||||
serde_json::json!({
|
}
|
||||||
"bash": "ask",
|
|
||||||
"edit": "ask"
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
root.insert(
|
root.insert(
|
||||||
"disabled_providers".to_owned(),
|
"disabled_providers".to_owned(),
|
||||||
serde_json::json!(["anthropic", "openai", "gemini", "ollama"]),
|
serde_json::json!(["anthropic", "openai", "gemini", "ollama"]),
|
||||||
@ -2751,6 +2751,7 @@ fn opencode_provider_config_json(
|
|||||||
api_key: &str,
|
api_key: &str,
|
||||||
project_root: &str,
|
project_root: &str,
|
||||||
runtime: Option<&McpRuntime>,
|
runtime: Option<&McpRuntime>,
|
||||||
|
eff: Option<&EffectivePermissions>,
|
||||||
) -> serde_json::Value {
|
) -> serde_json::Value {
|
||||||
let mut root = serde_json::Map::new();
|
let mut root = serde_json::Map::new();
|
||||||
root.insert(
|
root.insert(
|
||||||
@ -2811,13 +2812,9 @@ fn opencode_provider_config_json(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
root.insert(
|
if let Some(permission) = domain::opencode_permission_block(eff) {
|
||||||
"permission".to_owned(),
|
root.insert("permission".to_owned(), permission);
|
||||||
serde_json::json!({
|
}
|
||||||
"bash": "ask",
|
|
||||||
"edit": "ask"
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
serde_json::Value::Object(root)
|
serde_json::Value::Object(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4410,7 +4407,7 @@ command = "idea-mcp"
|
|||||||
domain::ports::SecretRef::new("secret-ref"),
|
domain::ports::SecretRef::new("secret-ref"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
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"];
|
let provider = &body["provider"]["anthropic"];
|
||||||
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
||||||
assert!(provider["options"].get("baseURL").is_none());
|
assert!(provider["options"].get("baseURL").is_none());
|
||||||
@ -4434,7 +4431,7 @@ command = "idea-mcp"
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.with_custom(custom);
|
.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);
|
||||||
let provider = &body["provider"]["my-custom"];
|
let provider = &body["provider"]["my-custom"];
|
||||||
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
||||||
assert_eq!(provider["options"]["baseURL"], "https://my-endpoint/v1");
|
assert_eq!(provider["options"]["baseURL"], "https://my-endpoint/v1");
|
||||||
@ -4458,10 +4455,90 @@ command = "idea-mcp"
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.with_custom(custom);
|
.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!(
|
assert_eq!(
|
||||||
body["provider"]["my-custom"]["models"]["my-model"]["name"],
|
body["provider"]["my-custom"]["models"]["my-model"]["name"],
|
||||||
"My Model"
|
"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();
|
||||||
|
|
||||||
|
use domain::permission::Posture;
|
||||||
|
let allowed =
|
||||||
|
opencode_config_json(&config, "/project", None, Some(&eff(Posture::Allow)));
|
||||||
|
assert_eq!(allowed["permission"]["bash"], "allow");
|
||||||
|
assert_eq!(allowed["permission"]["edit"], "allow");
|
||||||
|
|
||||||
|
let denied = opencode_config_json(&config, "/project", None, Some(&eff(Posture::Deny)));
|
||||||
|
assert_eq!(denied["permission"]["bash"], "deny");
|
||||||
|
assert_eq!(denied["permission"]["edit"], "deny");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opencode_provider_config_json_omits_permission_key_when_eff_is_none() {
|
||||||
|
let config = OpenCodeProviderConfig::new(
|
||||||
|
"anthropic",
|
||||||
|
"claude-sonnet-5",
|
||||||
|
domain::ports::SecretRef::new("secret-ref"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let body = opencode_provider_config_json(&config, "sk-live", "/project", None, None);
|
||||||
|
assert!(body.get("permission").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opencode_provider_config_json_projects_allow_and_deny() {
|
||||||
|
let config = OpenCodeProviderConfig::new(
|
||||||
|
"anthropic",
|
||||||
|
"claude-sonnet-5",
|
||||||
|
domain::ports::SecretRef::new("secret-ref"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -188,10 +188,11 @@ pub use layout::{
|
|||||||
pub use events::{DomainEvent, OrchestrationSource};
|
pub use events::{DomainEvent, OrchestrationSource};
|
||||||
|
|
||||||
pub use permission::{
|
pub use permission::{
|
||||||
render_permission_summary, resolve as resolve_permissions, AgentPermissionOverride, Capability,
|
opencode_permission_block, render_permission_summary, resolve as resolve_permissions,
|
||||||
CommandMatcher, CommandRule, Effect, EffectivePermissions, Glob, PathScope, PermissionError,
|
AgentPermissionOverride, Capability, CommandMatcher, CommandRule, Effect, EffectivePermissions,
|
||||||
PermissionProjection, PermissionProjector, PermissionRule, PermissionSet, Posture,
|
Glob, PathScope, PermissionError, PermissionProjection, PermissionProjector, PermissionRule,
|
||||||
ProjectPermissions, ProjectedFile, ProjectionContext, ProjectorKey, PERMISSIONS_VERSION,
|
PermissionSet, Posture, ProjectPermissions, ProjectedFile, ProjectionContext, ProjectorKey,
|
||||||
|
PERMISSIONS_VERSION,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use plugin::{
|
pub use plugin::{
|
||||||
|
|||||||
@ -640,6 +640,36 @@ impl EffectivePermissions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn posture_to_opencode(posture: Posture) -> &'static str {
|
||||||
|
match posture {
|
||||||
|
Posture::Allow => "allow",
|
||||||
|
Posture::Ask => "ask",
|
||||||
|
Posture::Deny => "deny",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the OpenCode `permission` config block from `eff` (ticket #94).
|
||||||
|
///
|
||||||
|
/// OpenCode's schema only exposes `bash`/`edit` keys (no `read`/`delete`: those
|
||||||
|
/// two capabilities are not expressible in it and are already enforced by the
|
||||||
|
/// Landlock sandbox). Mapping: `bash` ← [`EffectivePermissions::decide_bash`] on
|
||||||
|
/// the blanket command `""`; `edit` ← [`EffectivePermissions::decide_file`] on
|
||||||
|
/// [`Capability::Write`] with the blanket glob `"**"`.
|
||||||
|
///
|
||||||
|
/// `eff == None` (nothing posed) ⇒ [`None`]: the caller must omit the
|
||||||
|
/// `permission` key entirely, preserving OpenCode's native prompting — the same
|
||||||
|
/// invariant already enforced for Claude/Codex projection.
|
||||||
|
#[must_use]
|
||||||
|
pub fn opencode_permission_block(eff: Option<&EffectivePermissions>) -> Option<serde_json::Value> {
|
||||||
|
let eff = eff?;
|
||||||
|
let bash = posture_to_opencode(eff.decide_bash(""));
|
||||||
|
let edit = posture_to_opencode(eff.decide_file(Capability::Write, "**"));
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"bash": bash,
|
||||||
|
"edit": edit
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolves a project-level and an agent-level [`PermissionSet`] into the
|
/// Resolves a project-level and an agent-level [`PermissionSet`] into the
|
||||||
/// normalised [`EffectivePermissions`].
|
/// normalised [`EffectivePermissions`].
|
||||||
///
|
///
|
||||||
@ -1488,4 +1518,54 @@ mod tests {
|
|||||||
assert!(md.contains("OS-enforced") && md.contains("NOT OS-locked"));
|
assert!(md.contains("OS-enforced") && md.contains("NOT OS-locked"));
|
||||||
assert!(md.contains("**Default posture:** Deny"));
|
assert!(md.contains("**Default posture:** Deny"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- opencode_permission_block (ticket #94) --------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opencode_permission_block_none_when_nothing_posed() {
|
||||||
|
assert!(opencode_permission_block(None).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opencode_permission_block_maps_bash_from_decide_bash_and_edit_from_write() {
|
||||||
|
// Distinct rules per capability so bash/edit can't be conflated: bash is
|
||||||
|
// allowed, Write is denied, fallback stays Ask (never consulted here).
|
||||||
|
let set = PermissionSet::new(
|
||||||
|
vec![
|
||||||
|
PermissionRule::bash(Effect::Allow, vec![]),
|
||||||
|
PermissionRule::file(Capability::Write, Effect::Deny, path_scope(&["**"])).unwrap(),
|
||||||
|
],
|
||||||
|
Posture::Ask,
|
||||||
|
);
|
||||||
|
let eff = resolve(Some(&set), None).unwrap();
|
||||||
|
let block = opencode_permission_block(Some(&eff)).unwrap();
|
||||||
|
assert_eq!(block["bash"], "allow");
|
||||||
|
assert_eq!(block["edit"], "deny");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opencode_permission_block_falls_back_when_no_rule_matches() {
|
||||||
|
for (fallback, expected) in [
|
||||||
|
(Posture::Allow, "allow"),
|
||||||
|
(Posture::Ask, "ask"),
|
||||||
|
(Posture::Deny, "deny"),
|
||||||
|
] {
|
||||||
|
let set = PermissionSet::new(vec![], fallback);
|
||||||
|
let eff = resolve(Some(&set), None).unwrap();
|
||||||
|
let block = opencode_permission_block(Some(&eff)).unwrap();
|
||||||
|
assert_eq!(block["bash"], expected);
|
||||||
|
assert_eq!(block["edit"], expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opencode_permission_block_omits_read_and_delete_keys() {
|
||||||
|
let set = PermissionSet::new(vec![], Posture::Deny);
|
||||||
|
let eff = resolve(Some(&set), None).unwrap();
|
||||||
|
let block = opencode_permission_block(Some(&eff)).unwrap();
|
||||||
|
let obj = block.as_object().unwrap();
|
||||||
|
assert_eq!(obj.len(), 2, "only bash/edit are opencode-expressible");
|
||||||
|
assert!(!obj.contains_key("read"));
|
||||||
|
assert!(!obj.contains_key("delete"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,9 @@ use domain::ports::{SecretStore, SessionPlan};
|
|||||||
use domain::profile::{McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter};
|
use domain::profile::{McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter};
|
||||||
use domain::{
|
use domain::{
|
||||||
AgentProfile, AgentRuntime, AssistantContextError, AssistantContextProvider,
|
AgentProfile, AgentRuntime, AssistantContextError, AssistantContextProvider,
|
||||||
ContextInjectionPlan, FileSystem, FsError, Issue, IssueRef, MarkdownDoc, McpServerWiring,
|
ContextInjectionPlan, EffectivePermissions, FileSystem, FsError, Issue, IssueRef, MarkdownDoc,
|
||||||
PreparedContext, Project, ProjectPath, RemotePath, RuntimeError, StructuredSessionEnvironment,
|
McpServerWiring, PreparedContext, Project, ProjectPath, RemotePath, RuntimeError,
|
||||||
StructuredSessionEnvironmentPreparer,
|
StructuredSessionEnvironment, StructuredSessionEnvironmentPreparer,
|
||||||
};
|
};
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
@ -243,8 +243,14 @@ 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())
|
opencode_config_json(opencode, project.root.as_str(), runtime.as_ref(), None)
|
||||||
.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?;
|
||||||
@ -253,6 +259,7 @@ impl TicketAssistantEnvironmentPreparer {
|
|||||||
&api_key,
|
&api_key,
|
||||||
project.root.as_str(),
|
project.root.as_str(),
|
||||||
runtime.as_ref(),
|
runtime.as_ref(),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.to_string()
|
.to_string()
|
||||||
} else {
|
} else {
|
||||||
@ -337,6 +344,7 @@ fn opencode_config_json(
|
|||||||
config: &domain::profile::OpenCodeConfig,
|
config: &domain::profile::OpenCodeConfig,
|
||||||
project_root: &str,
|
project_root: &str,
|
||||||
runtime: Option<&McpRuntime>,
|
runtime: Option<&McpRuntime>,
|
||||||
|
eff: Option<&EffectivePermissions>,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let model = config.model.as_str();
|
let model = config.model.as_str();
|
||||||
let opencode_model = format!("llamacpp/{model}");
|
let opencode_model = format!("llamacpp/{model}");
|
||||||
@ -403,13 +411,9 @@ fn opencode_config_json(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
root.insert(
|
if let Some(permission) = domain::opencode_permission_block(eff) {
|
||||||
"permission".to_owned(),
|
root.insert("permission".to_owned(), permission);
|
||||||
json!({
|
}
|
||||||
"bash": "ask",
|
|
||||||
"edit": "ask"
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
root.insert(
|
root.insert(
|
||||||
"disabled_providers".to_owned(),
|
"disabled_providers".to_owned(),
|
||||||
json!(["anthropic", "openai", "gemini", "ollama"]),
|
json!(["anthropic", "openai", "gemini", "ollama"]),
|
||||||
@ -426,6 +430,7 @@ fn opencode_provider_config_json(
|
|||||||
api_key: &str,
|
api_key: &str,
|
||||||
project_root: &str,
|
project_root: &str,
|
||||||
runtime: Option<&McpRuntime>,
|
runtime: Option<&McpRuntime>,
|
||||||
|
eff: Option<&EffectivePermissions>,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
let mut root = Map::new();
|
let mut root = Map::new();
|
||||||
root.insert(
|
root.insert(
|
||||||
@ -491,13 +496,9 @@ fn opencode_provider_config_json(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
root.insert(
|
if let Some(permission) = domain::opencode_permission_block(eff) {
|
||||||
"permission".to_owned(),
|
root.insert("permission".to_owned(), permission);
|
||||||
json!({
|
}
|
||||||
"bash": "ask",
|
|
||||||
"edit": "ask"
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
Value::Object(root)
|
Value::Object(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -636,7 +637,7 @@ mod opencode_provider_config_json_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn known_provider_emits_only_the_api_key_option() {
|
fn known_provider_emits_only_the_api_key_option() {
|
||||||
let config = known_provider_config();
|
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"];
|
let provider = &body["provider"]["anthropic"];
|
||||||
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
||||||
assert!(provider["options"].get("baseURL").is_none());
|
assert!(provider["options"].get("baseURL").is_none());
|
||||||
@ -647,7 +648,7 @@ mod opencode_provider_config_json_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn custom_provider_emits_npm_base_url_and_models() {
|
fn custom_provider_emits_npm_base_url_and_models() {
|
||||||
let config = custom_provider_config();
|
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"];
|
let provider = &body["provider"]["my-custom"];
|
||||||
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
assert_eq!(provider["options"]["apiKey"], "sk-live");
|
||||||
assert_eq!(provider["options"]["baseURL"], "https://my-endpoint/v1");
|
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"))
|
let config = OpenCodeProviderConfig::new("my-custom", "my-model", SecretRef::new("ref"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.with_custom(custom);
|
.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!(
|
assert_eq!(
|
||||||
body["provider"]["my-custom"]["models"]["my-model"]["name"],
|
body["provider"]["my-custom"]["models"]["my-model"]["name"],
|
||||||
"My Model"
|
"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