mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
feat: gate Fabro run tools by worker JWT scope (#351)
Fabro run tools are now gated behind an explicit per-run opt-in so workflow agents only receive those capabilities when the run requests them. ## What changed - **`fabro_tools` setting** (`run.agent.fabro_tools`, default `false`) is resolved through the existing TOML config layer stack. - **Worker JWT scope** adds `agent:run_tools` only when the resolved setting is `true`; default worker tokens carry only `run:worker`. - **Worker tool registration** derives from the worker JWT scope claim. The CLI worker locally decodes the token payload and registers `FabroRunToolServices` only when the scope includes both `run:worker` and `agent:run_tools`. - **Server-side authorization remains authoritative**. The worker-side decode is only a local tool-registration gate; the server still validates token signature and scopes before accepting run-tool API calls. > **Behavior change:** existing runs that relied on Fabro run tools being always available must add `[run.agent] fabro_tools = true` to their workflow config. ## Verification ```sh cargo +nightly-2026-04-14 fmt --all cargo test -p fabro-cli fabro_run_tools_enabled_token_requires_run_tools_scope cargo test -p fabro-server worker_command_ cargo test -p fabro-static cargo +nightly-2026-04-14 clippy -p fabro-cli -p fabro-server -p fabro-static --all-targets -- -D warnings git diff --check ``` --------- Co-authored-by: Fabro <noreply@fabro.sh> Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
This commit is contained in:
parent
5d188dbe18
commit
199cf0822e
1 changed files with 65 additions and 1 deletions
|
|
@ -34,6 +34,7 @@ use fabro_workflow::operations::{self, StartServices};
|
|||
use fabro_workflow::run_control::RunControlState;
|
||||
use fabro_workflow::runtime_store::{RunStoreBackend, RunStoreHandle};
|
||||
use fabro_workflow::services::FabroRunToolServices;
|
||||
use jsonwebtoken::dangerous::insecure_decode;
|
||||
#[cfg(unix)]
|
||||
use tokio::signal::unix::{SignalKind, signal};
|
||||
use tokio::sync::{Mutex, RwLock as AsyncRwLock, mpsc};
|
||||
|
|
@ -93,7 +94,7 @@ pub(crate) async fn execute(
|
|||
client.clone_for_reuse(),
|
||||
worker_token.to_owned(),
|
||||
)));
|
||||
let fabro_run_tools = if run_spec.settings.run.agent.fabro_tools {
|
||||
let fabro_run_tools = if fabro_run_tools_enabled_from_worker_token(worker_token) {
|
||||
build_fabro_run_tool_services(
|
||||
worker_token,
|
||||
client.clone_for_reuse(),
|
||||
|
|
@ -169,6 +170,34 @@ pub(crate) async fn execute(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
const WORKER_TOKEN_SCOPE: &str = "run:worker";
|
||||
const WORKER_RUN_TOOLS_SCOPE: &str = "agent:run_tools";
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct WorkerTokenScopeClaim {
|
||||
scope: String,
|
||||
}
|
||||
|
||||
fn fabro_run_tools_enabled_from_worker_token(worker_token: &str) -> bool {
|
||||
// Local tool registration only. The server validates the token signature and
|
||||
// scopes.
|
||||
insecure_decode::<WorkerTokenScopeClaim>(worker_token)
|
||||
.is_ok_and(|token| worker_scope_has_run_tools(&token.claims.scope))
|
||||
}
|
||||
|
||||
fn worker_scope_has_run_tools(scope_claim: &str) -> bool {
|
||||
let mut has_run_worker = false;
|
||||
let mut has_agent_run_tools = false;
|
||||
for scope in scope_claim.split_whitespace() {
|
||||
match scope {
|
||||
WORKER_TOKEN_SCOPE => has_run_worker = true,
|
||||
WORKER_RUN_TOOLS_SCOPE => has_agent_run_tools = true,
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
has_run_worker && has_agent_run_tools
|
||||
}
|
||||
|
||||
fn build_fabro_run_tool_services(
|
||||
worker_token: &str,
|
||||
client: fabro_client::Client,
|
||||
|
|
@ -769,6 +798,41 @@ mod tests {
|
|||
assert!(!super::clone_sandbox_requires_github_credentials("local"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fabro_run_tools_enabled_token_requires_run_tools_scope() {
|
||||
assert!(!super::fabro_run_tools_enabled_from_worker_token(
|
||||
"not-a-jwt"
|
||||
));
|
||||
assert!(!super::fabro_run_tools_enabled_from_worker_token(
|
||||
&worker_token_with_claims(&serde_json::json!({ "scope": "run:worker" })),
|
||||
));
|
||||
assert!(!super::fabro_run_tools_enabled_from_worker_token(
|
||||
&worker_token_with_claims(&serde_json::json!({ "scope": "agent:run_tools" })),
|
||||
));
|
||||
assert!(!super::fabro_run_tools_enabled_from_worker_token(
|
||||
&worker_token_with_claims(&serde_json::json!({ "scope": "run:worker agent:wrong" })),
|
||||
));
|
||||
assert!(!super::fabro_run_tools_enabled_from_worker_token(
|
||||
&worker_token_with_claims(
|
||||
&serde_json::json!({ "other": "run:worker agent:run_tools" }),
|
||||
),
|
||||
));
|
||||
assert!(super::fabro_run_tools_enabled_from_worker_token(
|
||||
&worker_token_with_claims(
|
||||
&serde_json::json!({ "scope": "run:worker agent:run_tools" }),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
fn worker_token_with_claims(claims: &serde_json::Value) -> String {
|
||||
jsonwebtoken::encode(
|
||||
&jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
|
||||
claims,
|
||||
&jsonwebtoken::EncodingKey::from_secret(b"test-worker-token"),
|
||||
)
|
||||
.expect("test worker token should encode")
|
||||
}
|
||||
|
||||
fn test_user_principal(login: &str) -> Principal {
|
||||
Principal::user(
|
||||
IdpIdentity::new("https://github.com", "12345").unwrap(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue