mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-08 03:10:26 +00:00
Expose PermissionLevel on StageProjection and add sortable run columns (#373)
## Summary
Two independent additions landed together: surfacing `PermissionLevel`
on `StageProjection` (the primary goal), and making four previously
unsortable run-list columns (`repo`, `title`, `workflow`, `changes`)
sortable.
## Permission Level on StageProjection
`PermissionLevel` (`read-only | read-write | full`) was already resolved
at session start inside `fabro-agent` but never reached the API. This
wires it through the existing `agent.session.activated` event rather
than introducing a new event.
The data flow:
```mermaid
graph TB
A[SessionOptions.permission_level] -->|set at CLI build_tool_approval| B[Session.permission_level]
B -->|read in api.rs| C[ActivationLeaseOptions.permission_level]
C -->|emitted as| D[Event::AgentSessionActivated.permission_level]
D -->|convert.rs| E[EventBody::AgentSessionActivated.permission_level]
E -->|run_state.rs apply_event| F[StageProjection.permission_level]
F -->|OpenAPI + TS client| G[API consumers]
```
Key decisions:
- **No new event or type.** `PermissionLevel` is reused from
`fabro_types::session` directly; `AgentSessionActivatedProps` gains one
optional field with `skip_serializing_if`, so older persisted events
deserialize cleanly to `None`.
- **`Option<PermissionLevel>` on `StageProjection`** follows the same
pattern as `provider_used` — agent stages populate it, non-agent stages
leave it `None`. No migration required.
- **`AgentSessionActivatedProps` is now a progenitor type replacement**
so the API crate and the canonical type stay in sync (verified by the
new `agent_session_activated_props_round_trip` test).
### Plan Summary
- `fabro-agent` `config.rs` / `session.rs` — store and expose
`permission_level` on `SessionOptions`
- `fabro-types` `run_event/agent.rs` — add field to
`AgentSessionActivatedProps`
- `fabro-types` `run_projection.rs` — add field to `StageProjection`
- `fabro-workflow` `api.rs` / `activation_lease.rs` / `convert.rs` /
`events.rs` — thread the value to the emission site
- `fabro-store` `run_state.rs` — fold into projection on
`AgentSessionActivated`, plus new unit test
- OpenAPI schema, `fabro-api` build.rs, TS client — all
regenerated/updated
## Sortable Run Columns
`repo`, `title`, `workflow`, and `changes` columns were rendered as
plain `<th>` elements with no sort affordance. They now use `SortHeader`
in the frontend, the server-side `RunsSortKey` enum gains the four
variants, and the OpenAPI `ListRunsSortEnum` and TS client enum are
extended to match.
Sort helpers (`run_repo_key`, `run_title_key`, `run_workflow_key`,
`run_changes_total`) normalize to lowercase strings / integer totals and
compose with the existing stable ULID tiebreak.
### Fabro Details
<details>
<summary>Ran 9 stages in 53m 42s for $18.48</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 14s | – | 0 |
| preflight_lint | 2m 29s | – | 0 |
| implement | 20m 37s | $11.87 | 0 |
| simplify_opus | 8m 0s | $3.61 | 0 |
| simplify_gpt | 5m 11s | $2.50 | 0 |
| verify | 4m 48s | – | 0 |
| fixup | 9m 56s | $0.50 | 0 |
| **Total** | **53m 42s** | **$18.48** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: fabro-bot <fabro-bot@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
This commit is contained in:
parent
c81fcc2a27
commit
def37896cd
24 changed files with 295 additions and 20 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Arc<dyn ToolAccessPolicy>>,
|
||||
/// Agent tool permission level applied when the session started.
|
||||
pub permission_level: Option<PermissionLevel>,
|
||||
/// 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(|_| "<policy>"),
|
||||
)
|
||||
.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
|
||||
|
|
|
|||
|
|
@ -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<PermissionLevel> {
|
||||
self.config.permission_level
|
||||
}
|
||||
|
||||
/// Initialize session by discovering project docs and capturing environment
|
||||
/// context. Call before `process_input`.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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", &[]),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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::<ApiAgentSessionActivatedProps, AgentSessionActivatedProps>();
|
||||
}
|
||||
|
||||
#[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<T: 'static, U: 'static>() {
|
||||
assert_eq!(
|
||||
TypeId::of::<T>(),
|
||||
TypeId::of::<U>(),
|
||||
"{} should be the same type as {}",
|
||||
type_name::<T>(),
|
||||
type_name::<U>()
|
||||
);
|
||||
}
|
||||
|
|
@ -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::<ApiMcpServerProjection, McpServerProjection>();
|
||||
assert_same_type::<ApiMcpServerStatus, McpServerStatus>();
|
||||
assert_same_type::<ApiAgentMcpToolSummary, AgentMcpToolSummary>();
|
||||
assert_same_type::<ApiPermissionLevel, PermissionLevel>();
|
||||
}
|
||||
|
||||
#[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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<PermissionLevel>,
|
||||
) -> 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();
|
||||
|
|
|
|||
|
|
@ -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<ReasoningEffort>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub speed: Option<Speed>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub permission_level: Option<PermissionLevel>,
|
||||
pub capabilities: Vec<SessionCapability>,
|
||||
pub visit: u32,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SubAgentProjection>,
|
||||
#[serde(default, skip_serializing_if = "SkillsProjection::is_empty")]
|
||||
pub skills: SkillsProjection,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub permission_level: Option<PermissionLevel>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub mcp_servers: Vec<McpServerProjection>,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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<ReasoningEffort>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
speed: Option<Speed>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
permission_level: Option<PermissionLevel>,
|
||||
capabilities: Vec<fabro_types::SessionCapability>,
|
||||
},
|
||||
/// A stage's steerable live session binding ended.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
pub reasoning_effort: Option<ReasoningEffort>,
|
||||
pub speed: Option<Speed>,
|
||||
pub permission_level: Option<PermissionLevel>,
|
||||
pub capabilities: Vec<SessionCapability>,
|
||||
pub hub: Arc<SteeringHub>,
|
||||
pub emitter: Arc<Emitter>,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
44
lib/packages/fabro-api-client/src/models/agent-session-activated-props.ts
generated
Normal file
44
lib/packages/fabro-api-client/src/models/agent-session-activated-props.ts
generated
Normal file
|
|
@ -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<AgentSessionActivatedPropsCapabilitiesEnum>;
|
||||
'visit': number;
|
||||
}
|
||||
|
||||
export const AgentSessionActivatedPropsCapabilitiesEnum = {
|
||||
STEER: 'steer'
|
||||
} as const;
|
||||
|
||||
export type AgentSessionActivatedPropsCapabilitiesEnum = typeof AgentSessionActivatedPropsCapabilitiesEnum[keyof typeof AgentSessionActivatedPropsCapabilitiesEnum];
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
27
lib/packages/fabro-api-client/src/models/permission-level.ts
generated
Normal file
27
lib/packages/fabro-api-client/src/models/permission-level.ts
generated
Normal file
|
|
@ -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];
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue