diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index e10d6077f..045cc9f0a 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -5569,6 +5569,11 @@ components: type: string enum: [idle, running, failed] + PermissionLevel: + description: Agent tool permission level applied to a session. + type: string + enum: [read-only, read-write, full] + SessionTurn: description: Currently active durable session turn. type: object @@ -7561,6 +7566,40 @@ components: additionalProperties: true additionalProperties: true + AgentSessionActivatedProps: + description: Properties for the `agent.session.activated` event. + type: object + required: + - capabilities + - visit + properties: + thread_id: + type: ["string", "null"] + provider: + type: ["string", "null"] + model: + type: ["string", "null"] + reasoning_effort: + oneOf: + - $ref: "#/components/schemas/ReasoningEffort" + - type: "null" + speed: + oneOf: + - $ref: "#/components/schemas/BillingSpeed" + - type: "null" + permission_level: + oneOf: + - $ref: "#/components/schemas/PermissionLevel" + - type: "null" + capabilities: + type: array + items: + type: string + enum: [steer] + visit: + type: integer + minimum: 1 + RunSupersededByProps: description: Properties for the `run.superseded_by` audit event emitted on a rewound source run after archive succeeds. type: object @@ -7929,6 +7968,11 @@ components: skills: $ref: "#/components/schemas/SkillsProjection" description: Agent skills discovered and activated during this stage. + permission_level: + oneOf: + - $ref: "#/components/schemas/PermissionLevel" + - type: "null" + description: Agent tool permission level applied to this stage session. mcp_servers: type: array description: MCP servers observed by this stage. diff --git a/lib/crates/fabro-agent/src/cli.rs b/lib/crates/fabro-agent/src/cli.rs index 2ebcbc990..b736fdb3f 100644 --- a/lib/crates/fabro-agent/src/cli.rs +++ b/lib/crates/fabro-agent/src/cli.rs @@ -574,6 +574,7 @@ pub async fn run_with_args_and_client_and_catalog( let config = SessionOptions { tool_hooks: Some(tool_hooks.clone()), + permission_level: Some(permissions), skill_dirs: args.skills_dir.map(|d| vec![d]), mcp_servers, ..SessionOptions::default() @@ -591,6 +592,7 @@ pub async fn run_with_args_and_client_and_catalog( let factory_profile_kind = profile_kind; let factory_env = Arc::clone(&env); let factory_hooks = config.tool_hooks.clone(); + let factory_permission_level = config.permission_level; let factory: SessionFactory = Arc::new(move || { let child_summarizer = Some(build_summarizer( &factory_provider_id, @@ -611,6 +613,7 @@ pub async fn run_with_args_and_client_and_catalog( Arc::clone(&factory_env), SessionOptions { tool_hooks: factory_hooks.clone(), + permission_level: factory_permission_level, ..SessionOptions::default() }, None, diff --git a/lib/crates/fabro-agent/src/config.rs b/lib/crates/fabro-agent/src/config.rs index 06f8291f2..fb6bb52ab 100644 --- a/lib/crates/fabro-agent/src/config.rs +++ b/lib/crates/fabro-agent/src/config.rs @@ -4,6 +4,7 @@ use std::time::Duration; use fabro_llm::types::{ReasoningEffort, Speed}; use fabro_mcp::config::McpServerSettings; +use fabro_types::PermissionLevel; /// Callback invoked before each tool execution. Return `Ok(())` to allow, /// `Err(message)` to deny with the given message. @@ -121,6 +122,8 @@ pub struct SessionOptions { /// Static policy used to filter advertised tools and block hidden calls. /// `None` preserves legacy behavior: all registered tools are exposed. pub tool_access_policy: Option>, + /// Agent tool permission level applied when the session started. + pub permission_level: Option, /// Tool schema exposure mode used when `tool_access_policy` is set. pub tool_exposure_mode: ToolExposureMode, pub enable_context_compaction: bool, @@ -164,6 +167,7 @@ impl std::fmt::Debug for SessionOptions { "tool_access_policy", &self.tool_access_policy.as_ref().map(|_| ""), ) + .field("permission_level", &self.permission_level) .field("tool_exposure_mode", &self.tool_exposure_mode) .field("enable_context_compaction", &self.enable_context_compaction) .field( @@ -197,6 +201,7 @@ impl Default for SessionOptions { user_instructions: None, tool_hooks: None, tool_access_policy: None, + permission_level: None, tool_exposure_mode: ToolExposureMode::AutoApprovedOnly, enable_context_compaction: true, compaction_threshold_percent: 80, @@ -275,6 +280,7 @@ mod tests { assert_eq!(config.max_subagent_depth, 1); assert!(config.user_instructions.is_none()); assert!(config.tool_access_policy.is_none()); + assert!(config.permission_level.is_none()); assert_eq!( config.tool_exposure_mode, ToolExposureMode::AutoApprovedOnly diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs index de822bbbd..5bc8166ee 100644 --- a/lib/crates/fabro-agent/src/session.rs +++ b/lib/crates/fabro-agent/src/session.rs @@ -15,7 +15,7 @@ use fabro_llm::{Error as LlmError, retry}; use fabro_mcp::config::{McpServerSettings, McpTransport}; use fabro_mcp::connection_manager::McpConnectionManager; use fabro_model::{AgentProfileKind, Catalog, ModelRef, Speed}; -use fabro_types::{Principal, SessionMessage, SessionRecord, SteeringMessage}; +use fabro_types::{PermissionLevel, Principal, SessionMessage, SessionRecord, SteeringMessage}; use futures::StreamExt; use tokio::sync::{Mutex as AsyncMutex, Notify, broadcast}; use tokio::time; @@ -469,6 +469,11 @@ impl Session { self.config.speed } + #[must_use] + pub fn permission_level(&self) -> Option { + self.config.permission_level + } + /// Initialize session by discovering project docs and capturing environment /// context. Call before `process_input`. /// diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index 0998e3c31..6c18452a0 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -349,6 +349,12 @@ fn main() { ("CommandTermination", "fabro_types::CommandTermination", &[]), ("StageModelUsage", "fabro_types::StageModelUsage", &[]), ("StageProjection", "fabro_types::StageProjection", &[]), + ("PermissionLevel", "fabro_types::PermissionLevel", &[]), + ( + "AgentSessionActivatedProps", + "fabro_types::run_event::AgentSessionActivatedProps", + &[], + ), ("TodoListProjection", "fabro_types::TodoListProjection", &[]), ("SubAgentProjection", "fabro_types::SubAgentProjection", &[]), ("SubAgentStatus", "fabro_types::SubAgentStatus", &[]), diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs index c08f1d182..51e2437cb 100644 --- a/lib/crates/fabro-api/src/lib.rs +++ b/lib/crates/fabro-api/src/lib.rs @@ -18,6 +18,7 @@ pub mod types { Model, ModelCosts, ModelFeatures, ModelLimits, ModelRef as BillingModelRef, ModelTestMode, Provider, ReasoningEffort, ReasoningEffortFeature, Speed as BillingSpeed, }; + pub use fabro_types::run_event::AgentSessionActivatedProps; pub use fabro_types::settings::ServerNamespace; pub use fabro_types::settings::server::{ GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings, @@ -38,8 +39,8 @@ pub mod types { FailureSignature, GitContext, IdpIdentity, InterviewOption, InterviewQuestionRecord, McpServerProjection, McpServerStatus, PairId, PairMessageId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest, PairStatus, PairTarget, - PairTranscriptEntry, PairTranscriptResponse, PendingInterviewRecord, PreRunPushOutcome, - Principal, PullRequest, PullRequestDetails, PullRequestDetailsStatus, + PairTranscriptEntry, PairTranscriptResponse, PendingInterviewRecord, PermissionLevel, + PreRunPushOutcome, Principal, PullRequest, PullRequestDetails, PullRequestDetailsStatus, PullRequestDetailsUnavailableReason, PullRequestLink, PullRequestMeta, PullRequestResponse, QuestionType, RepositoryRef, Run, RunApproval, RunApprovalState, RunClientProvenance, RunEvent, RunEventDetailContentKind, RunEventDetailResponse, RunFailure, diff --git a/lib/crates/fabro-api/tests/agent_session_activated_props_round_trip.rs b/lib/crates/fabro-api/tests/agent_session_activated_props_round_trip.rs new file mode 100644 index 000000000..1cbf8dc04 --- /dev/null +++ b/lib/crates/fabro-api/tests/agent_session_activated_props_round_trip.rs @@ -0,0 +1,45 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::AgentSessionActivatedProps as ApiAgentSessionActivatedProps; +use fabro_types::run_event::AgentSessionActivatedProps; +use fabro_types::{PermissionLevel, ReasoningEffort, SessionCapability, Speed}; +use serde_json::json; + +#[test] +fn agent_session_activated_props_reuses_canonical_type() { + assert_same_type::(); +} + +#[test] +fn agent_session_activated_props_matches_openapi_json_shape() { + let value = json!({ + "thread_id": "thread-1", + "provider": "openai", + "model": "gpt-5.4", + "reasoning_effort": "high", + "speed": "fast", + "permission_level": "read-only", + "capabilities": ["steer"], + "visit": 1 + }); + + let props: AgentSessionActivatedProps = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(props.permission_level, Some(PermissionLevel::ReadOnly)); + assert_eq!(props.reasoning_effort, Some(ReasoningEffort::High)); + assert_eq!(props.speed, Some(Speed::Fast)); + assert_eq!(props.capabilities, vec![SessionCapability::Steer]); + assert_eq!(serde_json::to_value(&props).unwrap(), value); + + let api_props: ApiAgentSessionActivatedProps = serde_json::from_value(value).unwrap(); + assert_eq!(api_props, props); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/crates/fabro-api/tests/stage_projection_round_trip.rs b/lib/crates/fabro-api/tests/stage_projection_round_trip.rs index 87f9afd95..8b06a3255 100644 --- a/lib/crates/fabro-api/tests/stage_projection_round_trip.rs +++ b/lib/crates/fabro-api/tests/stage_projection_round_trip.rs @@ -4,14 +4,15 @@ use fabro_api::types::{ ActivatedSkill as ApiActivatedSkill, AgentMcpToolSummary as ApiAgentMcpToolSummary, AgentSkillActivationSource as ApiAgentSkillActivationSource, AgentSkillSummary as ApiAgentSkillSummary, McpServerProjection as ApiMcpServerProjection, - McpServerStatus as ApiMcpServerStatus, SkillsProjection as ApiSkillsProjection, - StageProjection as ApiStageProjection, SubAgentProjection as ApiSubAgentProjection, - SubAgentStatus as ApiSubAgentStatus, TodoListProjection as ApiTodoListProjection, + McpServerStatus as ApiMcpServerStatus, PermissionLevel as ApiPermissionLevel, + SkillsProjection as ApiSkillsProjection, StageProjection as ApiStageProjection, + SubAgentProjection as ApiSubAgentProjection, SubAgentStatus as ApiSubAgentStatus, + TodoListProjection as ApiTodoListProjection, }; use fabro_types::{ ActivatedSkill, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary, - McpServerProjection, McpServerStatus, SkillsProjection, StageProjection, SubAgentProjection, - SubAgentStatus, TodoListKind, TodoListProjection, + McpServerProjection, McpServerStatus, PermissionLevel, SkillsProjection, StageProjection, + SubAgentProjection, SubAgentStatus, TodoListKind, TodoListProjection, }; use serde_json::json; @@ -32,6 +33,7 @@ fn stage_projection_reuses_nested_agent_state_types() { assert_same_type::(); assert_same_type::(); assert_same_type::(); + assert_same_type::(); } #[test] @@ -113,6 +115,7 @@ fn stage_projection_round_trips_representative_json() { } ] }, + "permission_level": "read-only", "mcp_servers": [ { "server_name": "filesystem", @@ -135,6 +138,14 @@ fn stage_projection_round_trips_representative_json() { assert_eq!(serde_json::to_value(state).unwrap(), value); } +#[test] +fn permission_level_matches_openapi_json_shape() { + let permission_json = serde_json::to_value(PermissionLevel::ReadOnly).unwrap(); + assert_eq!(permission_json, json!("read-only")); + let api_permission: ApiPermissionLevel = serde_json::from_value(permission_json).unwrap(); + assert_eq!(api_permission, PermissionLevel::ReadOnly); +} + #[test] fn nested_agent_state_types_match_openapi_json_shape() { let todo_list = TodoListProjection::new(TodoListKind::OpenAiPlan, "openai_plan:ses_root"); diff --git a/lib/crates/fabro-server/src/server/tests.rs b/lib/crates/fabro-server/src/server/tests.rs index 2861a6bec..89c13c59e 100644 --- a/lib/crates/fabro-server/src/server/tests.rs +++ b/lib/crates/fabro-server/src/server/tests.rs @@ -9459,6 +9459,7 @@ fn active_steerable_stage_projection_ignores_stale_deactivation() { model: Some("gpt-5.4".to_string()), reasoning_effort: None, speed: None, + permission_level: None, capabilities: vec![SessionCapability::Steer], }); update_live_run_from_event(&state, run_id, &activated_a); @@ -9481,6 +9482,7 @@ fn active_steerable_stage_projection_ignores_stale_deactivation() { model: Some("gpt-5.4".to_string()), reasoning_effort: None, speed: None, + permission_level: None, capabilities: vec![SessionCapability::Steer], }); update_live_run_from_event(&state, run_id, &activated_b); @@ -9539,6 +9541,7 @@ async fn steer_with_active_acp_session_forwards_to_worker() { model: None, reasoning_effort: None, speed: None, + permission_level: None, capabilities: vec![SessionCapability::Steer], }); update_live_run_from_event(&state, run_id, &activated); @@ -9642,6 +9645,7 @@ async fn active_acp_steerable_marker_clears_on_terminal_paths() { model: None, reasoning_effort: None, speed: None, + permission_level: None, capabilities: vec![SessionCapability::Steer], }); update_live_run_from_event(&state, run_id, &activated); diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs index f30e10598..cd47792c8 100644 --- a/lib/crates/fabro-store/src/run_state.rs +++ b/lib/crates/fabro-store/src/run_state.rs @@ -406,6 +406,7 @@ impl RunProjectionReducer for RunProjection { return Ok(()); }; stage.provider_used = Some(StageModelUsage::from_agent_session_activated(props)); + stage.permission_level = props.permission_level; } // `AgentAcpStarted` is the start-of-process signal for an external // ACP agent. `provider_used` is intentionally sourced from the @@ -1105,10 +1106,11 @@ mod tests { use fabro_types::{ AgentBackend, BilledModelUsage, BilledTokenCounts, BlockedReason, Checkpoint, CheckpointRecord, CommandTermination, EventBody, FailureCategory, FailureDetail, - FailureReason, Graph, McpServerStatus, Outcome, PendingReason, PullRequestLink, - QuestionType, ReasoningEffort, RunApprovalState, RunBlobId, RunControlAction, RunDiff, - RunEvent, RunSize, RunSpec, RunStatus, Speed, StageModelUsage, StageOutcome, StageState, - SubAgentStatus, SuccessReason, WorkflowSettings, first_event_seq, fixtures, + FailureReason, Graph, McpServerStatus, Outcome, PendingReason, PermissionLevel, + PullRequestLink, QuestionType, ReasoningEffort, RunApprovalState, RunBlobId, + RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, RunStatus, Speed, StageModelUsage, + StageOutcome, StageState, SubAgentStatus, SuccessReason, WorkflowSettings, first_event_seq, + fixtures, }; use serde_json::json; @@ -1618,6 +1620,7 @@ mod tests { model: Some("gpt-5.4".to_string()), reasoning_effort: Some(ReasoningEffort::High), speed: Some(Speed::Fast), + permission_level: None, capabilities: vec![fabro_types::SessionCapability::Steer], visit: 1, }), @@ -1714,6 +1717,7 @@ mod tests { model: Some("fake".to_string()), reasoning_effort: None, speed: None, + permission_level: None, capabilities: vec![fabro_types::SessionCapability::Steer], visit: 1, }), @@ -4039,6 +4043,52 @@ mod tests { ); } + #[test] + fn agent_session_activation_updates_stage_permission_level_projection() { + fn activated_props( + permission_level: Option, + ) -> AgentSessionActivatedProps { + AgentSessionActivatedProps { + thread_id: None, + provider: Some("openai".to_string()), + model: Some("gpt-5.4".to_string()), + reasoning_effort: None, + speed: None, + permission_level, + capabilities: vec![], + visit: 1, + } + } + + let mut state = initialized_projection(); + let stage_id = stage_id(); + + state + .apply_event(&test_stage_event( + 1, + EventBody::AgentSessionActivated(activated_props(Some( + PermissionLevel::ReadOnly, + ))), + stage_id.clone(), + )) + .unwrap(); + + let stage = state.stage(&stage_id).unwrap(); + assert_eq!(stage.permission_level, Some(PermissionLevel::ReadOnly)); + + let mut legacy_state = initialized_projection(); + legacy_state + .apply_event(&test_stage_event( + 1, + EventBody::AgentSessionActivated(activated_props(None)), + stage_id.clone(), + )) + .unwrap(); + + let legacy_stage = legacy_state.stage(&stage_id).unwrap(); + assert_eq!(legacy_stage.permission_level, None); + } + #[test] fn mcp_server_events_update_stage_projection() { let mut state = initialized_projection(); diff --git a/lib/crates/fabro-types/src/run_event/agent.rs b/lib/crates/fabro-types/src/run_event/agent.rs index cbf865496..15f5eff86 100644 --- a/lib/crates/fabro-types/src/run_event/agent.rs +++ b/lib/crates/fabro-types/src/run_event/agent.rs @@ -4,7 +4,9 @@ use serde_json::Value; use super::BilledTokenCounts; use crate::transcript::{ToolCall, ToolResult, TranscriptMessage}; -use crate::{MessageId, ModelRef, PairId, PairMessageId, PairSystemMessageKind, TurnId}; +use crate::{ + MessageId, ModelRef, PairId, PairMessageId, PairSystemMessageKind, PermissionLevel, TurnId, +}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct AgentSessionStartedProps { @@ -39,6 +41,8 @@ pub struct AgentSessionActivatedProps { pub reasoning_effort: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub speed: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub permission_level: Option, pub capabilities: Vec, pub visit: u32, } diff --git a/lib/crates/fabro-types/src/run_projection.rs b/lib/crates/fabro-types/src/run_projection.rs index 1f0aa11d5..c439b1178 100644 --- a/lib/crates/fabro-types/src/run_projection.rs +++ b/lib/crates/fabro-types/src/run_projection.rs @@ -9,9 +9,9 @@ use crate::run_event::{AgentSessionActivatedProps, StagePromptProps}; use crate::{ AgentBackend, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary, BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord, InvalidTransition, - ModelRef, PullRequestLink, RunApproval, RunControlAction, RunDiff, RunId, RunSandbox, RunSpec, - RunStatus, RunTiming, StageCompletion, StageHandler, StageId, StageState, StageTiming, - StartRecord, TodoListProjection, + ModelRef, PermissionLevel, PullRequestLink, RunApproval, RunControlAction, RunDiff, RunId, + RunSandbox, RunSpec, RunStatus, RunTiming, StageCompletion, StageHandler, StageId, StageState, + StageTiming, StartRecord, TodoListProjection, }; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -156,6 +156,8 @@ pub struct StageProjection { pub subagents: Vec, #[serde(default, skip_serializing_if = "SkillsProjection::is_empty")] pub skills: SkillsProjection, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub permission_level: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub mcp_servers: Vec, pub state: StageState, @@ -232,6 +234,7 @@ impl StageProjection { todos: None, subagents: Vec::new(), skills: SkillsProjection::default(), + permission_level: None, mcp_servers: Vec::new(), provider_used: None, diff: None, diff --git a/lib/crates/fabro-workflow/src/event/convert.rs b/lib/crates/fabro-workflow/src/event/convert.rs index 2deda7c35..6a975a0e2 100644 --- a/lib/crates/fabro-workflow/src/event/convert.rs +++ b/lib/crates/fabro-workflow/src/event/convert.rs @@ -1148,6 +1148,7 @@ fn event_body_from_event(event: &Event) -> EventBody { model, reasoning_effort, speed, + permission_level, capabilities, visit, .. @@ -1157,6 +1158,7 @@ fn event_body_from_event(event: &Event) -> EventBody { model: model.clone(), reasoning_effort: *reasoning_effort, speed: *speed, + permission_level: *permission_level, capabilities: capabilities.clone(), visit: *visit, }), diff --git a/lib/crates/fabro-workflow/src/event/events.rs b/lib/crates/fabro-workflow/src/event/events.rs index 84f02de1f..e91686d22 100644 --- a/lib/crates/fabro-workflow/src/event/events.rs +++ b/lib/crates/fabro-workflow/src/event/events.rs @@ -3,9 +3,10 @@ use std::collections::BTreeMap; use ::fabro_types::{ BilledTokenCounts, BlockedReason, CommandTermination, DiffSummary, FailureReason, ForkSourceRef, GitContext, PairId, PairMessageId, PairSystemMessageKind, PairTarget, - ParallelBranchId, PendingReason, Principal, PullRequestLink, RunBlobId, RunFailure, RunId, - RunNoticeLevel, RunPairEndedReason, RunPairFailedReason, RunProvenance, RunRunnableSource, - RunTiming, SandboxProvider, StageId, StageTiming, SuccessReason, run_event as fabro_types, + ParallelBranchId, PendingReason, PermissionLevel, Principal, PullRequestLink, RunBlobId, + RunFailure, RunId, RunNoticeLevel, RunPairEndedReason, RunPairFailedReason, RunProvenance, + RunRunnableSource, RunTiming, SandboxProvider, StageId, StageTiming, SuccessReason, + run_event as fabro_types, }; use fabro_agent::{AgentEvent, SandboxEvent}; use fabro_model::{ReasoningEffort, Speed}; @@ -614,6 +615,8 @@ pub enum Event { reasoning_effort: Option, #[serde(default, skip_serializing_if = "Option::is_none")] speed: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + permission_level: Option, capabilities: Vec, }, /// A stage's steerable live session binding ended. diff --git a/lib/crates/fabro-workflow/src/handler/agent.rs b/lib/crates/fabro-workflow/src/handler/agent.rs index 0788ee1e5..f0fe9f9ff 100644 --- a/lib/crates/fabro-workflow/src/handler/agent.rs +++ b/lib/crates/fabro-workflow/src/handler/agent.rs @@ -813,6 +813,7 @@ mod tests { model: Some("gpt-5.4".to_string()), reasoning_effort: Some(ReasoningEffort::High), speed: Some(Speed::Fast), + permission_level: None, capabilities: vec![fabro_types::SessionCapability::Steer], }, &scope, diff --git a/lib/crates/fabro-workflow/src/handler/llm/acp.rs b/lib/crates/fabro-workflow/src/handler/llm/acp.rs index 827f09e8a..4b3d890b6 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/acp.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/acp.rs @@ -276,6 +276,7 @@ impl AgentAcpBackend { model: config_name.map(str::to_string), reasoning_effort: None, speed: None, + permission_level: None, capabilities: vec![SessionCapability::Steer], hub: Arc::clone(steering_hub), emitter: Arc::clone(emitter), diff --git a/lib/crates/fabro-workflow/src/handler/llm/activation_lease.rs b/lib/crates/fabro-workflow/src/handler/llm/activation_lease.rs index 0c6b45454..f4475651a 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/activation_lease.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/activation_lease.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use fabro_model::{ReasoningEffort, Speed}; -use fabro_types::{SessionCapability, StageId}; +use fabro_types::{PermissionLevel, SessionCapability, StageId}; use crate::error::Error; use crate::event::{Emitter, Event}; @@ -24,6 +24,7 @@ pub struct ActivationLeaseOptions { pub model: Option, pub reasoning_effort: Option, pub speed: Option, + pub permission_level: Option, pub capabilities: Vec, pub hub: Arc, pub emitter: Arc, @@ -59,6 +60,7 @@ impl ActivationLease { model: options.model, reasoning_effort: options.reasoning_effort, speed: options.speed, + permission_level: options.permission_level, capabilities: options.capabilities, }); options @@ -160,6 +162,7 @@ mod tests { model: Some("gpt-5.4".to_string()), reasoning_effort: None, speed: None, + permission_level: None, capabilities: vec![SessionCapability::Steer], hub, emitter, diff --git a/lib/crates/fabro-workflow/src/handler/llm/api.rs b/lib/crates/fabro-workflow/src/handler/llm/api.rs index 8da02640d..ccbea933e 100644 --- a/lib/crates/fabro-workflow/src/handler/llm/api.rs +++ b/lib/crates/fabro-workflow/src/handler/llm/api.rs @@ -707,6 +707,7 @@ impl AgentApiBackend { let factory_env = Arc::clone(sandbox); let factory_tool_env = tool_env.cloned(); let factory_fabro_run_tools = fabro_run_tools.clone(); + let factory_permission_level = config.permission_level; let factory: SessionFactory = Arc::new(move || { let mut child_profile = build_profile( &factory_model, @@ -725,6 +726,7 @@ impl AgentApiBackend { SessionOptions { reasoning_effort: controls.reasoning_effort, speed: controls.speed, + permission_level: factory_permission_level, ..SessionOptions::default() }, None, @@ -781,6 +783,7 @@ impl AgentApiBackend { model: Some(session.model().to_string()), reasoning_effort: session.reasoning_effort(), speed: session.speed(), + permission_level: session.permission_level(), capabilities: vec![SessionCapability::Steer], hub: Arc::clone(&self.steering_hub), emitter: Arc::clone(emitter), diff --git a/lib/crates/fabro-workflow/src/operations/fork.rs b/lib/crates/fabro-workflow/src/operations/fork.rs index 94b290199..b4ed04d30 100644 --- a/lib/crates/fabro-workflow/src/operations/fork.rs +++ b/lib/crates/fabro-workflow/src/operations/fork.rs @@ -305,6 +305,7 @@ mod tests { model: Some("gpt-5.4".to_string()), reasoning_effort: None, speed: None, + permission_level: None, capabilities: vec![fabro_types::SessionCapability::Steer], visit: 1, }) diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 8891a6b4f..e619a94f7 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -24,6 +24,7 @@ index.ts models/activated-skill.ts models/agent-mcp-tool-summary.ts models/agent-permissions.ts +models/agent-session-activated-props.ts models/agent-skill-activation-source.ts models/agent-skill-summary.ts models/aggregate-billing-totals.ts @@ -219,6 +220,7 @@ models/pair-transcript-user-message.ts models/pair-transcript-warning.ts models/pending-interview-record.ts models/pending-reason.ts +models/permission-level.ts models/pre-run-push-outcome-failed.ts models/pre-run-push-outcome-not-attempted.ts models/pre-run-push-outcome-skipped-no-remote.ts diff --git a/lib/packages/fabro-api-client/src/models/agent-session-activated-props.ts b/lib/packages/fabro-api-client/src/models/agent-session-activated-props.ts new file mode 100644 index 000000000..77a3d829f --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/agent-session-activated-props.ts @@ -0,0 +1,44 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { BillingSpeed } from './billing-speed'; +// May contain unused imports in some cases +// @ts-ignore +import type { PermissionLevel } from './permission-level'; +// May contain unused imports in some cases +// @ts-ignore +import type { ReasoningEffort } from './reasoning-effort'; + +/** + * Properties for the `agent.session.activated` event. + */ +export interface AgentSessionActivatedProps { + 'thread_id'?: string | null; + 'provider'?: string | null; + 'model'?: string | null; + 'reasoning_effort'?: ReasoningEffort | null; + 'speed'?: BillingSpeed | null; + 'permission_level'?: PermissionLevel | null; + 'capabilities': Array; + 'visit': number; +} + +export const AgentSessionActivatedPropsCapabilitiesEnum = { + STEER: 'steer' +} as const; + +export type AgentSessionActivatedPropsCapabilitiesEnum = typeof AgentSessionActivatedPropsCapabilitiesEnum[keyof typeof AgentSessionActivatedPropsCapabilitiesEnum]; diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 4ea2bd295..5c9f13346 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -1,6 +1,7 @@ export * from './activated-skill'; export * from './agent-mcp-tool-summary'; export * from './agent-permissions'; +export * from './agent-session-activated-props'; export * from './agent-skill-activation-source'; export * from './agent-skill-summary'; export * from './aggregate-billing'; @@ -195,6 +196,7 @@ export * from './pair-transcript-user-message'; export * from './pair-transcript-warning'; export * from './pending-interview-record'; export * from './pending-reason'; +export * from './permission-level'; export * from './pre-run-push-outcome'; export * from './pre-run-push-outcome-failed'; export * from './pre-run-push-outcome-not-attempted'; diff --git a/lib/packages/fabro-api-client/src/models/permission-level.ts b/lib/packages/fabro-api-client/src/models/permission-level.ts new file mode 100644 index 000000000..e006d64f8 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/permission-level.ts @@ -0,0 +1,27 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Agent tool permission level applied to a session. + */ + +export const PermissionLevel = { + READ_ONLY: 'read-only', + READ_WRITE: 'read-write', + FULL: 'full' +} as const; + +export type PermissionLevel = typeof PermissionLevel[keyof typeof PermissionLevel]; diff --git a/lib/packages/fabro-api-client/src/models/stage-projection.ts b/lib/packages/fabro-api-client/src/models/stage-projection.ts index cdec8e984..c98a7a285 100644 --- a/lib/packages/fabro-api-client/src/models/stage-projection.ts +++ b/lib/packages/fabro-api-client/src/models/stage-projection.ts @@ -27,6 +27,9 @@ import type { CommandTermination } from './command-termination'; import type { McpServerProjection } from './mcp-server-projection'; // May contain unused imports in some cases // @ts-ignore +import type { PermissionLevel } from './permission-level'; +// May contain unused imports in some cases +// @ts-ignore import type { SkillsProjection } from './skills-projection'; // May contain unused imports in some cases // @ts-ignore @@ -89,6 +92,7 @@ export interface StageProjection { * Agent skills discovered and activated during this stage. */ 'skills'?: SkillsProjection; + 'permission_level'?: PermissionLevel | null; /** * MCP servers observed by this stage. */