This commit is contained in:
Brandon McNama 2026-10-16 08:00:50 -04:00 • committed by GitHub
commit 7d5f1afe79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 786 additions and 50 deletions

View file

@ -17,7 +17,7 @@ Fabro only reads `settings.toml`. Older `server.toml`, `user.toml`, and `cli.tom
| Scope | Examples |
|---|---|
| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.sandbox]`, `[server.storage]`, `[server.artifacts]`, `[server.scheduler]`, `[server.logging]`, `[server.integrations]` |
| Server-owned (runtime-only from local `settings.toml`) | `[server.listen]`, `[server.api]`, `[server.web]`, `[server.auth]`, `[server.sandbox]`, `[server.storage]`, `[server.artifacts]`, `[server.scheduler]`, `[server.approvals]`, `[server.logging]`, `[server.integrations]` |
| Shared run defaults (layered through `.fabro/project.toml`/`workflow.toml`) | `[run.model]`, `[run.prepare]`, `[run.environment]`, `[environments.<slug>]`, `[run.checkpoint]`, `[run.inputs]`, `[run.pull_request]`, `[run.git]`, `[run.hooks]`, `[run.agent]` |
The CLI-only `[cli.*]` sections (including `[cli.target]`) belong in the client machine's `settings.toml`. They tell CLI commands how to reach a server. The server process does not read `[cli.*]` for its own binding or routing.
@ -76,6 +76,11 @@ region = "us-east-1"
[server.scheduler]
max_concurrent_runs = 8
# Server half of the child auto-approval opt-in; a parent workflow must also
# set `[run.children] auto_approve = true` for its children to skip approval.
[server.approvals]
allow_child_auto_approval = true
[server.logging]
level = "info"
@ -279,10 +284,20 @@ The `[run.*]` sections in `settings.toml` act as defaults for every run.
Run creation uses the registered workflow version's `workflow.toml` above the server defaults, with explicit `RunIntent` arguments at highest precedence. The same rules apply to local and remote clients. Project and user config are not submitted to the create endpoint. Manifest-based preflight, validation, and graph previews still accept their bundled configuration layers.
Server-owned values like `[server.storage]`, `[server.api]`, `[server.web]`, and `[server.scheduler]` come from the server machine's `settings.toml` or `fabro server start` flags. Environments and MCP server references resolve against the server's catalogs.
Server-owned values like `[server.storage]`, `[server.api]`, `[server.web]`, `[server.scheduler]`, and `[server.approvals]` come from the server machine's `settings.toml` or `fabro server start` flags. Environments and MCP server references resolve against the server's catalogs.
Merge rules follow the normative matrix: TOML `[run.inputs]` tables replace wholesale, explicit intent inputs merge per key at highest precedence, environment `env` and `labels` merge by key, `[run.prepare.steps]` replaces whole-list, and `[[run.hooks]]` merge by optional `id`. Most other fields use "higher-precedence wins" field-wise merging.
### `[server.approvals]` section
Server-owned approval policy for worker-started child runs.
| Key | Description | Default |
|---|---|---|
| `allow_child_auto_approval` | Server half of the child auto-approval opt-in | `false` |
Worker-started child runs enter `pending` with `approval_required` unless both this setting and the spawning parent workflow's `[run.children] auto_approve` are enabled. The policy only affects future start requests; a pending child is never retroactively approved by a configuration change. See [Child Runs — Start and approval](/execution/child-runs#start-and-approval).
### `[server.logging]` section
Configure the server log level and destination.

View file

@ -9463,6 +9463,7 @@ components:
enum:
- start_requested
- approved
- auto_approved
FailureCategory:
description: Product-level classification for grouping and retry policy.
@ -15446,6 +15447,7 @@ components:
- storage
- artifacts
- scheduler
- approvals
- logging
- integrations
properties:
@ -15465,6 +15467,8 @@ components:
$ref: "#/components/schemas/ServerArtifactsSettings"
scheduler:
$ref: "#/components/schemas/ServerSchedulerSettings"
approvals:
$ref: "#/components/schemas/ServerApprovalsSettings"
logging:
$ref: "#/components/schemas/ServerLoggingSettings"
integrations:
@ -15638,6 +15642,19 @@ components:
max_concurrent_runs:
type: integer
ServerApprovalsSettings:
description: |
Server-owned approval policy. `allow_child_auto_approval` is the
server half of the child auto-approval opt-in; the workflow half is
`[run.children] auto_approve` in the parent workflow's
`workflow.toml`, and a worker-started child is auto-approved only
when both are enabled.
type: object
required: [allow_child_auto_approval]
properties:
allow_child_auto_approval:
type: boolean
ServerLoggingSettings:
type: object
required: [level, destination]
@ -15792,6 +15809,7 @@ components:
- pull_request
- artifacts
- integrations
- children
properties:
goal:
oneOf:
@ -15845,6 +15863,19 @@ components:
$ref: "#/components/schemas/ArtifactsSettings"
integrations:
$ref: "#/components/schemas/RunIntegrationsSettings"
children:
$ref: "#/components/schemas/RunChildrenSettings"
RunChildrenSettings:
description: |
How this workflow treats the child runs its workers start. Governs
the child's lifecycle start decision, not the child's own
agent-interaction approval posture (`run.execution.approval`).
type: object
required: [auto_approve]
properties:
auto_approve:
type: boolean
RunIntegrationsSettings:
type: object

View file

@ -121,7 +121,34 @@ Child run creation and child run execution are separate steps:
4. **Schedule** - after approval, the child becomes `runnable` and the scheduler starts it when capacity is available.
5. **Execute** - the child runs its own workflow and writes its own events, checkpoints, artifacts, and outputs.
The approval step prevents a workflow agent from recursively launching executing runs without an operator checkpoint. Approve or deny pending child runs from the web UI or through the run lifecycle API.
By default, the approval step keeps a workflow agent from recursively launching executing runs without an operator checkpoint. Approve or deny pending child runs from the web UI or through the run lifecycle API.
### Auto-approval
A parent workflow can opt its child starts out of the operator checkpoint with two keys that must both be enabled:
```toml title=".fabro/workflows/dispatcher/workflow.toml"
[run.children]
auto_approve = true
```
```toml title="settings.toml (server)"
[server.approvals]
allow_child_auto_approval = true
```
- `[run.children] auto_approve` belongs to the **parent** workflow: it governs children dispatched by that workflow. It is read from the registered workflow configuration (and the server's `[run]` defaults below it), never from run inputs or `args`.
- `[server.approvals] allow_child_auto_approval` is the server-owned half, default `false`.
When both are enabled, a child start records an `auto_approved` lifecycle transition and becomes `runnable` in one pass: its approval state reads `approved` with a decision timestamp, and it never projects a transient `pending`. Any other combination keeps the operator checkpoint.
The policy is evaluated when the start request arrives. Turning either key off only affects future starts; already-pending children stay pending and already-runnable children are not revoked.
A worker start of a run that is not its own child always requires operator approval, whatever the policy says: a run-management worker can request the start, but never past the approval predicate.
<Note>
`args.auto_approve` on `fabro_run_create` is a different knob. It maps to the child run's own `[run.execution] approval` setting and controls the child's *agent-interaction* approval posture, not its lifecycle start. Setting `args.auto_approve` alone never skips the child-start checkpoint.
</Note>
## Supervise children

View file

@ -108,6 +108,9 @@ type = "sandbox"
command = ["npx", "@playwright/mcp@latest", "--port", "3100", "--headless"]
port = 3100
[run.children]
auto_approve = true
[run.pull_request]
enabled = true
draft = false
@ -489,6 +492,23 @@ Each collection reflects the post-stage workspace state rather than filesystem m
Fabro resolves the configured workspace root but does not recurse through symlinks below it while collecting artifacts. Dependency, cache, and build directories such as `.git`, `node_modules`, `target`, `.venv`, `.cache`, and `dist` are pruned. A collection is limited to 100 files, 10 MB per file, and 50 MB total.
### `[run.children]`
Configure how this workflow treats the [child runs](/execution/child-runs) its workers start.
```toml title="run.toml"
[run.children]
auto_approve = true
```
| Field | Description | Default |
|---|---|---|
| `auto_approve` | Auto-approve children this workflow spawns at start | `false` |
This setting belongs to the parent workflow and is the workflow half of the child auto-approval opt-in. A worker-started child only skips the operator approval checkpoint when the server also enables `[server.approvals] allow_child_auto_approval`; any other combination leaves the child pending approval. See [Child Runs — Start and approval](/execution/child-runs#start-and-approval).
`auto_approve` here governs the child's lifecycle start. It is distinct from `args.auto_approve` on `fabro_run_create` (the child's own `[run.execution] approval` posture for agent interactions) and from `[run.execution] approval`, which applies to this run's own agent stages.
### `[run.agent]`
Configure workflow agent behavior that is not tied to a single stage.

View file

@ -3769,6 +3769,7 @@ fn apply_lifecycle_to_managed_run(state: &AppState, run_id: RunId, record: &RunL
RunLifecycleKind::Runnable
| RunLifecycleKind::StartRequested
| RunLifecycleKind::Approved
| RunLifecycleKind::AutoApproved
| RunLifecycleKind::Denied
| RunLifecycleKind::CancelRequested
| RunLifecycleKind::PauseRequested

View file

@ -119,6 +119,59 @@ pub(in crate::server) async fn queue_run_start(
queue_run(state, id, &run_state, resume, actor).await
}
/// How a non-resume start is decided for the run's approval gate.
enum ChildStartDecision {
/// Not a worker-started child: queue as runnable, unchanged.
NotGated,
/// A worker-started child waiting for operator approval.
ApprovalRequired,
/// A worker-started child approved by policy.
AutoApproved,
}
/// Decide whether a non-resume start of `run_state` needs operator approval.
///
/// A worker-started child is auto-approved only when the server enables the
/// capability and the spawning parent's persisted workflow settings opt in.
/// A worker start of any other run fails closed to the approval gate: the
/// run-management path may still request it, but never past the predicate.
/// The global setting is read here, at the start decision, so a config
/// reload affects future starts only.
async fn child_start_decision(
state: &AppState,
run_state: &fabro_store::RunProjection,
actor: &Principal,
) -> Result<ChildStartDecision, ApiError> {
let Principal::Worker { run_id } = actor else {
return Ok(ChildStartDecision::NotGated);
};
if run_state.spec.run_id == *run_id {
// A worker's own run — a resume or self-management — keeps its path.
return Ok(ChildStartDecision::NotGated);
}
if run_state.parent_id != Some(*run_id) {
// Not this worker's child: there is no spawning parent whose policy
// could approve it, so operator approval is required.
return Ok(ChildStartDecision::ApprovalRequired);
}
if !state.server_settings().server.approvals.allow_child_auto_approval {
return Ok(ChildStartDecision::ApprovalRequired);
}
// The spawning parent owns the policy: read it from the parent's own
// persisted settings, not from the child's parent link, so reassigning
// the link cannot change the decision. A parent whose projection is gone
// fails closed to the approval gate.
let parent = run_records::projection(state, *run_id)
.await
.map_err(|err| ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
let opted_in = parent.is_some_and(|parent| parent.spec.settings.run.children.auto_approve);
Ok(if opted_in {
ChildStartDecision::AutoApproved
} else {
ChildStartDecision::ApprovalRequired
})
}
/// Queue the run for the scheduler: record that its start was requested
/// and that it is runnable (or pending approval), and register it as a
/// managed run in start or resume mode. The caller has checked that the run
@ -136,11 +189,12 @@ pub(super) async fn queue_run(
.root()
.to_path_buf();
let dot_source = run_state.spec.graph_source.clone().unwrap_or_default();
let approval_required = !resume
&& matches!(
&actor,
Principal::Worker { run_id } if run_state.parent_id == Some(*run_id)
);
let decision = if resume {
ChildStartDecision::NotGated
} else {
child_start_decision(state, run_state, &actor).await?
};
let approval_required = matches!(decision, ChildStartDecision::ApprovalRequired);
let mut start_requested = RunLifecycleRecord::new(RunLifecycleKind::StartRequested);
start_requested.source = Some(if resume { "resume" } else { "start" }.to_string());
let next_status = if approval_required {
@ -150,12 +204,25 @@ pub(super) async fn queue_run(
} else {
RunStatus::Runnable
};
let next = if approval_required {
run_records::transition(RunLifecycleKind::Pending, next_status)
} else {
run_records::runnable(RunRunnableSource::StartRequested)
let records = match decision {
ChildStartDecision::NotGated => vec![
start_requested,
run_records::runnable(RunRunnableSource::StartRequested),
],
ChildStartDecision::ApprovalRequired => vec![
start_requested,
run_records::transition(RunLifecycleKind::Pending, next_status),
],
// Policy approval is recorded as its own auditable transition before
// the runnable state, so the projection never shows a transitive
// pending state for an auto-approved child.
ChildStartDecision::AutoApproved => vec![
start_requested,
RunLifecycleRecord::new(RunLifecycleKind::AutoApproved),
run_records::runnable(RunRunnableSource::AutoApproved),
],
};
for record in [start_requested, next] {
for record in records {
if let Err(err) = run_records::lifecycle(state, id, record).await {
return Err(ApiError::new(
StatusCode::INTERNAL_SERVER_ERROR,

View file

@ -7989,6 +7989,314 @@ async fn denying_pending_child_run_fails_with_approval_denied() {
assert!(denied_body["lifecycle"]["approval"]["denial_reason"].is_null());
}
const CHILD_AUTO_APPROVE_TOML: &str = "_version = 1\n\n[run.children]\nauto_approve = true\n";
/// A JWT-authenticated app whose server settings carry the child
/// auto-approval opt-in.
fn child_policy_app(allow_child_auto_approval: bool) -> (Arc<AppState>, Router) {
let source = format!(
r#"
_version = 1
[server.auth]
methods = ["dev-token"]
[server.approvals]
allow_child_auto_approval = {allow_child_auto_approval}
"#
);
let state = test_app_state_with_session_key(
server_settings_from_toml(&source),
manifest_run_defaults_from_toml(&source),
Some(TEST_SESSION_SECRET),
);
let app = build_router(Arc::clone(&state), jwt_auth_mode());
(state, app)
}
async fn create_run_with_workflow_config(
app: &Router,
bearer: &str,
workflow_toml: Option<&str>,
) -> RunId {
let intent = test_intent_with_bearer(
app,
"workflow.fabro",
MINIMAL_DOT,
workflow_toml,
Some(bearer),
)
.await;
let response = app
.clone()
.oneshot(json_bearer_request(Method::POST, "/runs", bearer, &intent))
.await
.unwrap();
let body = response_json!(response, StatusCode::CREATED).await;
body["id"].as_str().unwrap().parse::<RunId>().unwrap()
}
async fn create_child_as_worker(
app: &Router,
worker_token: &str,
parent_run_id: RunId,
registrar_bearer: &str,
) -> RunId {
let mut child_intent = test_intent_with_bearer(
app,
"workflow.fabro",
MINIMAL_DOT,
None,
Some(registrar_bearer),
)
.await;
child_intent["parent_id"] = json!(parent_run_id.to_string());
let response = app
.clone()
.oneshot(json_bearer_request(
Method::POST,
"/runs",
worker_token,
&child_intent,
))
.await
.unwrap();
let body = response_json!(response, StatusCode::CREATED).await;
body["id"].as_str().unwrap().parse::<RunId>().unwrap()
}
async fn start_run_as_worker(app: &Router, worker_token: &str, run_id: RunId) -> Response {
app.clone()
.oneshot(json_bearer_request(
Method::POST,
&format!("/runs/{run_id}/start"),
worker_token,
&json!({ "resume": false }),
))
.await
.unwrap()
}
async fn child_lifecycle_transitions(state: &AppState, run_id: RunId) -> Vec<String> {
state
.stores
.run_summaries
.platform_records()
.read(&run_id)
.await
.unwrap()
.iter()
.filter_map(|stored| match &stored.record {
PlatformRecord::RunLifecycle(record) => Some(record.transition.to_string()),
_ => None,
})
.collect()
}
#[tokio::test]
async fn child_start_stays_pending_when_only_the_workflow_opts_in() {
let (state, app) = jwt_auth_app();
let user_jwt = issue_test_user_jwt();
let parent_run_id =
create_run_with_workflow_config(&app, &user_jwt, Some(CHILD_AUTO_APPROVE_TOML)).await;
let worker_token = issue_test_run_tools_worker_token(&parent_run_id);
let child_run_id =
create_child_as_worker(&app, &worker_token, parent_run_id, &user_jwt).await;
let pending_body = response_json!(
start_run_as_worker(&app, &worker_token, child_run_id).await,
StatusCode::OK
)
.await;
assert_eq!(
run_json_status(&pending_body),
&json!({
"kind": "pending",
"reason": "approval_required"
})
);
assert_eq!(
pending_body["lifecycle"]["approval"]["state"].as_str(),
Some("pending")
);
let runs = state.runs.lock().expect("runs lock poisoned");
assert_eq!(
runs.get(&child_run_id).map(|run| run.status),
Some(RunStatus::Pending {
reason: fabro_types::PendingReason::ApprovalRequired,
})
);
}
#[tokio::test]
async fn child_start_stays_pending_when_only_the_server_opts_in() {
let (state, app) = child_policy_app(true);
let user_jwt = issue_test_user_jwt();
let parent_run_id = create_run_with_workflow_config(&app, &user_jwt, None).await;
let worker_token = issue_test_run_tools_worker_token(&parent_run_id);
let child_run_id =
create_child_as_worker(&app, &worker_token, parent_run_id, &user_jwt).await;
let pending_body = response_json!(
start_run_as_worker(&app, &worker_token, child_run_id).await,
StatusCode::OK
)
.await;
assert_eq!(
run_json_status(&pending_body),
&json!({
"kind": "pending",
"reason": "approval_required"
})
);
let runs = state.runs.lock().expect("runs lock poisoned");
assert_eq!(
runs.get(&child_run_id).map(|run| run.status),
Some(RunStatus::Pending {
reason: fabro_types::PendingReason::ApprovalRequired,
})
);
}
#[tokio::test]
async fn child_start_is_auto_approved_when_both_opt_ins_are_enabled() {
let (state, app) = child_policy_app(true);
let user_jwt = issue_test_user_jwt();
let parent_run_id =
create_run_with_workflow_config(&app, &user_jwt, Some(CHILD_AUTO_APPROVE_TOML)).await;
let worker_token = issue_test_run_tools_worker_token(&parent_run_id);
let child_run_id =
create_child_as_worker(&app, &worker_token, parent_run_id, &user_jwt).await;
let approved_body = response_json!(
start_run_as_worker(&app, &worker_token, child_run_id).await,
StatusCode::OK
)
.await;
assert_eq!(
run_json_status(&approved_body),
&json!({ "kind": "runnable" })
);
assert_eq!(
approved_body["lifecycle"]["approval"]["state"].as_str(),
Some("approved")
);
assert!(
approved_body["lifecycle"]["approval"]["decided_at"]
.as_str()
.is_some()
);
// The lifecycle history records one deliberate policy approval; the
// projection never shows a transient pending state.
let transitions = child_lifecycle_transitions(state.as_ref(), child_run_id).await;
assert!(transitions.contains(&"auto_approved".to_string()));
assert!(!transitions.contains(&"pending".to_string()));
let runs = state.runs.lock().expect("runs lock poisoned");
assert_eq!(
runs.get(&child_run_id).map(|run| run.status),
Some(RunStatus::Runnable)
);
}
#[tokio::test]
async fn child_start_ignores_client_auto_approve_args_without_policy() {
let (_state, app) = jwt_auth_app();
let user_jwt = issue_test_user_jwt();
let parent_run_id = create_run_with_bearer(&app, &user_jwt).await;
let worker_token = issue_test_run_tools_worker_token(&parent_run_id);
let mut child_intent = test_intent_with_bearer(
&app,
"workflow.fabro",
MINIMAL_DOT,
None,
Some(&user_jwt),
)
.await;
child_intent["parent_id"] = json!(parent_run_id.to_string());
child_intent["args"] = json!({ "auto_approve": true });
let response = app
.clone()
.oneshot(json_bearer_request(
Method::POST,
"/runs",
&worker_token,
&child_intent,
))
.await
.unwrap();
let child_body = response_json!(response, StatusCode::CREATED).await;
let child_run_id = child_body["id"].as_str().unwrap().parse::<RunId>().unwrap();
let pending_body = response_json!(
start_run_as_worker(&app, &worker_token, child_run_id).await,
StatusCode::OK
)
.await;
assert_eq!(
run_json_status(&pending_body),
&json!({
"kind": "pending",
"reason": "approval_required"
})
);
}
#[tokio::test]
async fn worker_start_of_runs_it_did_not_spawn_fails_closed_to_approval() {
let (state, app) = jwt_auth_app();
let user_jwt = issue_test_user_jwt();
let parent_run_id = create_run_with_bearer(&app, &user_jwt).await;
let unrelated_run_id = create_run_with_bearer(&app, &user_jwt).await;
let worker_token = issue_test_run_tools_worker_token(&parent_run_id);
// A worker can still create an unparented run through the generic route
// (the run tools link parents through the management route), but the
// start decision treats it as any other unrelated run.
let created = create_run_with_bearer(&app, &worker_token).await;
let pending_body = response_json!(
start_run_as_worker(&app, &worker_token, created).await,
StatusCode::OK
)
.await;
assert_eq!(
run_json_status(&pending_body),
&json!({
"kind": "pending",
"reason": "approval_required"
})
);
// Starting a run the worker did not spawn fails closed to the approval
// gate instead of skipping it.
let pending_body = response_json!(
start_run_as_worker(&app, &worker_token, unrelated_run_id).await,
StatusCode::OK
)
.await;
assert_eq!(
run_json_status(&pending_body),
&json!({
"kind": "pending",
"reason": "approval_required"
})
);
let projection = state
.load_run_projection(&unrelated_run_id)
.await
.unwrap();
assert_eq!(
projection.status,
RunStatus::Pending {
reason: fabro_types::PendingReason::ApprovalRequired,
}
);
}
#[tokio::test]
async fn patch_run_title_rejects_invalid_titles() {
let app = test_app_with();

View file

@ -221,6 +221,25 @@ fn fold_lifecycle(projection: &mut RunProjection, record: &RunLifecycleRecord, a
approval.decided_at = Some(at);
}
}
Kind::AutoApproved => {
// A child start approved by policy: no request preceded the
// decision, so the approval is created approved in one step
// rather than projected through a transient pending state.
match projection.approval.as_mut() {
Some(approval) => {
approval.state = RunApprovalState::Approved;
approval.decided_at = Some(at);
}
None => {
projection.approval = Some(RunApproval {
state: RunApprovalState::Approved,
requested_at: at,
decided_at: Some(at),
denial_reason: None,
});
}
}
}
Kind::Denied => {
if let Some(approval) = projection.approval.as_mut() {
approval.state = RunApprovalState::Denied;

View file

@ -310,6 +310,7 @@ pub enum RunLifecycleKind {
StartRequested,
Pending,
Approved,
AutoApproved,
Denied,
Runnable,
Starting,

View file

@ -267,6 +267,11 @@ fn main() {
"fabro_types::settings::run::RunModelControls",
&[],
),
(
"RunChildrenSettings",
"fabro_types::settings::run::RunChildrenSettings",
&[],
),
(
"ServerSandboxSettings",
"fabro_types::settings::server::ServerSandboxSettings",
@ -307,6 +312,11 @@ fn main() {
"fabro_types::settings::server::ServerSchedulerSettings",
&[],
),
(
"ServerApprovalsSettings",
"fabro_types::settings::server::ServerApprovalsSettings",
&[],
),
(
"ServerLoggingSettings",
"fabro_types::settings::server::ServerLoggingSettings",

View file

@ -20,16 +20,17 @@ pub mod types {
};
pub use fabro_environment::Environment;
pub use fabro_types::settings::run::{
McpHttpProtocol, RunIntegrationsGithubSettings, RunIntegrationsSettings, RunModelControls,
RunModelSettings,
McpHttpProtocol, RunChildrenSettings, RunIntegrationsGithubSettings,
RunIntegrationsSettings, RunModelControls, RunModelSettings,
};
pub use fabro_types::settings::server::{
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
LogDestination, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings,
ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings,
ServerListenSettings, ServerLoggingSettings, ServerSandboxProviderSettings,
ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings,
ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, WebhookStrategy,
LogDestination, ObjectStoreSettings, ServerApiSettings, ServerApprovalsSettings,
ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,
ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings,
ServerSandboxProviderSettings, ServerSandboxProvidersSettings, ServerSandboxSettings,
ServerSchedulerSettings, ServerStorageSettings, ServerWebSettings,
SlackIntegrationSettings, WebhookStrategy,
};
pub use fabro_types::settings::{McpTransport, ServerNamespace};
pub use fabro_types::status::{

View file

@ -2,7 +2,7 @@ use std::any::{TypeId, type_name};
use fabro_api::types::{
LogDestination as ApiLogDestination, ObjectStoreSettings as ApiObjectStoreSettings,
ServerNamespace as ApiServerNamespace,
ServerApprovalsSettings as ApiServerApprovalsSettings, ServerNamespace as ApiServerNamespace,
ServerSandboxProviderSettings as ApiServerSandboxProviderSettings,
ServerSandboxProvidersSettings as ApiServerSandboxProvidersSettings,
ServerSandboxSettings as ApiServerSandboxSettings, ServerSettings as ApiServerSettings,
@ -11,7 +11,7 @@ use fabro_config::ServerSettingsBuilder;
use fabro_types::ServerSettings;
use fabro_types::settings::ServerNamespace;
use fabro_types::settings::server::{
LogDestination, ObjectStoreSettings, ServerSandboxProviderSettings,
LogDestination, ObjectStoreSettings, ServerApprovalsSettings, ServerSandboxProviderSettings,
ServerSandboxProvidersSettings, ServerSandboxSettings,
};
@ -24,6 +24,7 @@ fn server_settings_family_reuses_domain_types() {
assert_same_type::<ApiServerSandboxSettings, ServerSandboxSettings>();
assert_same_type::<ApiServerSandboxProvidersSettings, ServerSandboxProvidersSettings>();
assert_same_type::<ApiServerSandboxProviderSettings, ServerSandboxProviderSettings>();
assert_same_type::<ApiServerApprovalsSettings, ServerApprovalsSettings>();
}
#[test]
@ -76,6 +77,10 @@ strategy = "tailscale_funnel"
assert_eq!(json["server"]["listen"]["address"], "127.0.0.1:32276");
assert_eq!(json["server"]["storage"]["root"], "/srv/fabro");
assert_eq!(json["server"]["logging"]["destination"], "stdout");
assert_eq!(
json["server"]["approvals"]["allow_child_auto_approval"],
false
);
assert_eq!(
json["server"]["sandbox"]["providers"]["local"]["enabled"],
true

View file

@ -1,8 +1,11 @@
use std::any::{TypeId, type_name};
use fabro_api::types::WorkflowSettings as ApiWorkflowSettings;
use fabro_api::types::{
RunChildrenSettings as ApiRunChildrenSettings, WorkflowSettings as ApiWorkflowSettings,
};
use fabro_config::{EnvironmentLayer, MergeMap, RunLayer, SettingsLayer, WorkflowSettingsBuilder};
use fabro_types::WorkflowSettings;
use fabro_types::settings::run::RunChildrenSettings;
fn seeded_environment_catalog() -> MergeMap<EnvironmentLayer> {
r#"
@ -29,6 +32,7 @@ fn workflow_settings_from_toml(source: &str) -> WorkflowSettings {
#[test]
fn workflow_settings_family_reuses_domain_types() {
assert_same_type::<ApiWorkflowSettings, WorkflowSettings>();
assert_same_type::<ApiRunChildrenSettings, RunChildrenSettings>();
}
#[test]
@ -105,6 +109,42 @@ fn workflow_settings_default_run_checkpoint_skip_git_hooks_is_false() {
assert_eq!(json["run"]["checkpoint"]["skip_git_hooks"], false);
}
#[test]
fn workflow_settings_run_children_auto_approve_defaults_to_false() {
let settings = workflow_settings_from_toml("_version = 1\n");
let json = serde_json::to_value(&settings).expect("workflow settings should serialize");
assert_eq!(json["run"]["children"]["auto_approve"], false);
assert!(!settings.run.children.auto_approve);
}
#[test]
fn workflow_settings_run_children_auto_approve_round_trips() {
let settings = workflow_settings_from_toml(
r#"
_version = 1
[run.children]
auto_approve = true
"#,
);
let json = serde_json::to_value(&settings).expect("workflow settings should serialize");
assert_eq!(json["run"]["children"]["auto_approve"], true);
let round_trip: ApiWorkflowSettings =
serde_json::from_value(json).expect("workflow settings should deserialize");
assert_eq!(round_trip, settings);
assert!(round_trip.run.children.auto_approve);
}
#[test]
fn persisted_run_settings_without_children_field_deserialize_to_default() {
// Run specs persisted before `[run.children]` existed omit the field.
let legacy: RunChildrenSettings = serde_json::from_value(serde_json::json!({}))
.expect("legacy run children settings should deserialize");
assert!(!legacy.auto_approve);
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),

View file

@ -39,6 +39,9 @@ url = "http://localhost:3000"
[server.scheduler]
max_concurrent_runs = 5
[server.approvals]
allow_child_auto_approval = false
[server.artifacts]
provider = "local"
prefix = ""

View file

@ -28,15 +28,15 @@ pub use run::{
GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer,
InterviewsLayer, McpEntryLayer, ModelRefOrSplice, NotificationProviderLayer,
NotificationRouteLayer, PrepareStep, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer,
RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer,
RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer,
RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, RunScmLayer, ScmGitHubLayer,
StringOrSplice,
RunChildrenLayer, RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer,
RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer,
RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer,
RunScmLayer, ScmGitHubLayer, StringOrSplice,
};
pub use server::{
GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer,
ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer,
ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
ServerApiLayer, ServerApprovalsLayer, ServerArtifactsLayer, ServerAuthGithubLayer,
ServerAuthLayer, ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer,
ServerSchedulerLayer, ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer,
};

View file

@ -62,6 +62,21 @@ pub struct RunLayer {
pub artifacts: Option<RunArtifactsLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub integrations: Option<RunIntegrationsLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub children: Option<RunChildrenLayer>,
}
/// `[run.children]` — how this workflow treats the child runs its workers
/// start. Belongs to the parent workflow: it governs children dispatched by
/// that workflow, never the run's own agent interactions
/// (`[run.execution] approval`).
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
pub struct RunChildrenLayer {
/// Workflow half of the child auto-approval opt-in; also requires the
/// server's `[server.approvals] allow_child_auto_approval`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_approve: Option<bool>,
}
/// `[run.integrations]` — run-level integration knobs.

View file

@ -33,6 +33,8 @@ pub struct ServerLayer {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scheduler: Option<ServerSchedulerLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approvals: Option<ServerApprovalsLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logging: Option<ServerLoggingLayer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub integrations: Option<ServerIntegrationsLayer>,
@ -195,6 +197,16 @@ pub struct ServerSchedulerLayer {
pub max_concurrent_runs: Option<usize>,
}
/// `[server.approvals]` — server-owned approval policy.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]
pub struct ServerApprovalsLayer {
/// Server half of the child auto-approval opt-in. The workflow half is
/// `[run.children] auto_approve` in the parent workflow's `workflow.toml`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_child_auto_approval: Option<bool>,
}
/// `[server.logging]` — process-owned logging configuration for the server.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
#[serde(deny_unknown_fields)]

View file

@ -47,12 +47,13 @@ pub use layers::{
HookTlsMode, IntegrationWebhooksLayer, InterviewProviderLayer, InterviewsLayer, LlmLayer,
LogFilter, McpEntryLayer, MergeMap, ModelRefOrSplice, NotificationProviderLayer,
NotificationRouteLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, PrepareStep, ProjectLayer,
ReplaceMap, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer,
RunEnvironmentLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer,
RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer,
RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, RunScmLayer, ScmGitHubLayer,
ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer,
ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
ReplaceMap, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunChildrenLayer,
RunCloneLayer, RunEnvironmentLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer,
RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer,
RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer,
RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerApprovalsLayer, ServerArtifactsLayer,
ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerLayer,
ServerListenLayer, ServerLoggingLayer,
ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer,
ServerSchedulerLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer, SlackIntegrationLayer,
StickyMap, StringOrSplice, WorkflowLayer,

View file

@ -7,10 +7,11 @@ use fabro_types::settings::run::{
ArtifactsSettings, GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings,
McpServerSettings, McpTransport, MergeStrategy, NotificationProviderSettings,
NotificationRouteSettings, PreparedStep, PreparedStepRun, PullRequestSettings,
ResolvedMcpEntry, RunAgentSettings, RunBranchSettings, RunCheckpointSettings, RunCloneSettings,
RunExecutionSettings, RunGitSettings, RunGoal, RunIntegrationsGithubSettings,
RunIntegrationsSettings, RunInterviewsSettings, RunModelControls, RunModelSettings,
RunNamespace, RunPrepareSettings, RunScmSettings, ScmGitHubSettings, TlsMode,
ResolvedMcpEntry, RunAgentSettings, RunBranchSettings, RunCheckpointSettings,
RunChildrenSettings, RunCloneSettings, RunExecutionSettings, RunGitSettings, RunGoal,
RunIntegrationsGithubSettings, RunIntegrationsSettings, RunInterviewsSettings,
RunModelControls, RunModelSettings, RunNamespace, RunPrepareSettings, RunScmSettings,
ScmGitHubSettings, TlsMode,
};
use fabro_util::workspace_glob::WorkspaceGlob;
@ -18,10 +19,10 @@ use super::{ResolveError, resolve_run_environment};
use crate::{
EnvironmentLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer,
InterviewsLayer, McpEntryLayer, MergeMap, ModelRefOrSplice, NotificationProviderLayer,
NotificationRouteLayer, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer,
RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer,
RunLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, RunRunBranchLayer, RunScmLayer,
StickyMap, StringOrSplice,
NotificationRouteLayer, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunChildrenLayer,
RunCloneLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer,
RunIntegrationsLayer, RunLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer,
RunRunBranchLayer, RunScmLayer, StickyMap, StringOrSplice,
};
pub fn resolve_run(
@ -74,6 +75,15 @@ pub fn resolve_run(
pull_request,
artifacts: resolve_artifacts(layer.artifacts.as_ref(), errors),
integrations: resolve_integrations(layer.integrations.as_ref(), errors),
children: resolve_children(layer.children.as_ref()),
}
}
fn resolve_children(children: Option<&RunChildrenLayer>) -> RunChildrenSettings {
RunChildrenSettings {
auto_approve: children
.and_then(|children| children.auto_approve)
.unwrap_or(false),
}
}

View file

@ -5,11 +5,11 @@ use fabro_types::SandboxProviderKind;
use fabro_types::settings::server::{
GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings,
ObjectStoreProvider, ObjectStoreSettings, SandboxPluginSettings, ServerApiSettings,
ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings,
ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings, ServerNamespace,
ServerSandboxProviderSettings, ServerSandboxProvidersSettings, ServerSandboxSettings,
ServerSchedulerSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings,
WebhookStrategy,
ServerApprovalsSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod,
ServerAuthSettings, ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings,
ServerNamespace, ServerSandboxProviderSettings, ServerSandboxProvidersSettings,
ServerSandboxSettings, ServerSchedulerSettings, ServerStorageSettings, ServerWebSettings,
SlackIntegrationSettings, WebhookStrategy,
};
use fabro_util::Home;
@ -50,6 +50,13 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> Se
.and_then(|scheduler| scheduler.max_concurrent_runs)
.expect("defaults.toml should provide server.scheduler.max_concurrent_runs"),
},
approvals: ServerApprovalsSettings {
allow_child_auto_approval: layer
.approvals
.as_ref()
.and_then(|approvals| approvals.allow_child_auto_approval)
.unwrap_or(false),
},
logging: ServerLoggingSettings {
level: layer
.logging

View file

@ -2000,3 +2000,28 @@ command = ["fs-server"]
);
}
}
#[test]
fn run_children_auto_approve_defaults_to_false() {
let settings = super::workflow_settings_from_layer(SettingsLayer::default())
.expect("empty settings should resolve")
.run;
assert!(!settings.children.auto_approve);
}
#[test]
fn run_children_auto_approve_resolves_from_workflow_toml() {
let settings = super::workflow_settings_from_toml(
r#"
_version = 1
[run.children]
auto_approve = true
"#,
)
.expect("[run.children] should resolve")
.run;
assert!(settings.children.auto_approve);
}

View file

@ -681,3 +681,27 @@ methods = ["dev-token", "github"]
assert!(dev_token_auth_enabled(&both));
assert!(!dev_token_auth_enabled(&SettingsLayer::default()));
}
#[test]
fn child_auto_approval_is_disabled_by_default() {
let settings = resolve_server(&empty_settings_with_auth_methods());
assert!(!settings.approvals.allow_child_auto_approval);
}
#[test]
fn child_auto_approval_resolves_from_server_config() {
let layer = parse(
r#"
_version = 1
[server.auth]
methods = ["dev-token"]
[server.approvals]
allow_child_auto_approval = true
"#,
);
assert!(resolve_server(&layer).approvals.allow_child_auto_approval);
}

View file

@ -45,6 +45,7 @@ pub struct RunNamespace {
pub pull_request: Option<PullRequestSettings>,
pub artifacts: ArtifactsSettings,
pub integrations: RunIntegrationsSettings,
pub children: RunChildrenSettings,
}
#[expect(
@ -74,6 +75,7 @@ impl Default for RunNamespace {
pull_request: None,
artifacts: ArtifactsSettings::default(),
integrations: RunIntegrationsSettings::default(),
children: RunChildrenSettings::default(),
}
}
}
@ -597,6 +599,24 @@ pub struct RunIntegrationsSettings {
pub github: RunIntegrationsGithubSettings,
}
/// `[run.children]` — how this workflow treats the child runs its workers
/// start.
///
/// This governs the child's *lifecycle* start decision (whether the child
/// waits for operator approval), not the child's own agent-interaction
/// approval posture (`run.execution.approval`, which `RunIntentArgs
/// .auto_approve` maps to). It is the workflow half of the child
/// auto-approval opt-in; the server half is `server.approvals
/// .allow_child_auto_approval`, and both must be enabled.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct RunChildrenSettings {
/// Whether children this workflow spawns are auto-approved at start.
/// Absent or `false` keeps every worker-started child pending operator
/// approval.
pub auto_approve: bool,
}
/// `[run.integrations.github]` — runtime GitHub token shape.
///
/// `permissions` is empty when no token should be requested. The

View file

@ -30,6 +30,7 @@ pub struct ServerNamespace {
pub storage: ServerStorageSettings,
pub artifacts: ServerArtifactsSettings,
pub scheduler: ServerSchedulerSettings,
pub approvals: ServerApprovalsSettings,
pub logging: ServerLoggingSettings,
pub integrations: ServerIntegrationsSettings,
}
@ -50,6 +51,7 @@ impl ServerNamespace {
storage: ServerStorageSettings::default(),
artifacts: ServerArtifactsSettings::default(),
scheduler: ServerSchedulerSettings::default(),
approvals: ServerApprovalsSettings::default(),
logging: ServerLoggingSettings::default(),
integrations: ServerIntegrationsSettings::default(),
}
@ -256,6 +258,19 @@ pub struct ServerSchedulerSettings {
pub max_concurrent_runs: usize,
}
/// `[server.approvals]` — server-owned approval policy.
///
/// The policy is only half of the child auto-approval opt-in: a workflow must
/// also declare `[run.children] auto_approve` for a worker-started child to
/// skip the operator approval gate.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServerApprovalsSettings {
/// Whether policy approval of worker-started child runs is available on
/// this server at all. Absent or `false` keeps every child start on the
/// pending-approval path.
pub allow_child_auto_approval: bool,
}
#[derive(
Debug,
Clone,

View file

@ -600,7 +600,8 @@ mod tests {
}
}
/// What made a run runnable: its start request, or its approval.
/// What made a run runnable: its start request, its approval, or a policy
/// that approved its child start.
#[derive(
Debug,
Clone,
@ -618,4 +619,5 @@ mod tests {
pub enum RunRunnableSource {
StartRequested,
Approved,
AutoApproved,
}

View file

@ -380,6 +380,7 @@ models/run-artifact.ts
models/run-branch-settings.ts
models/run-checkpoint-settings.ts
models/run-checkpoint.ts
models/run-children-settings.ts
models/run-client-provenance.ts
models/run-clone-settings.ts
models/run-commit-parent.ts
@ -485,6 +486,7 @@ models/secret-list-response.ts
models/secret-metadata.ts
models/secret-type.ts
models/server-api-settings.ts
models/server-approvals-settings.ts
models/server-artifacts-settings.ts
models/server-auth-github-settings.ts
models/server-auth-method.ts

View file

@ -351,6 +351,7 @@ export * from './run-artifact-list-response';
export * from './run-branch-settings';
export * from './run-checkpoint';
export * from './run-checkpoint-settings';
export * from './run-children-settings';
export * from './run-client-provenance';
export * from './run-clone-settings';
export * from './run-commit';
@ -455,6 +456,7 @@ export * from './secret-list-response';
export * from './secret-metadata';
export * from './secret-type';
export * from './server-api-settings';
export * from './server-approvals-settings';
export * from './server-artifacts-settings';
export * from './server-auth-github-settings';
export * from './server-auth-method';

View file

@ -0,0 +1,22 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* How this workflow treats the child runs its workers start. Governs the child\'s lifecycle start decision, not the child\'s own agent-interaction approval posture (`run.execution.approval`).
*/
export interface RunChildrenSettings {
'auto_approve': boolean;
}

View file

@ -36,6 +36,9 @@ import type { RunBranchSettings } from './run-branch-settings';
import type { RunCheckpointSettings } from './run-checkpoint-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { RunChildrenSettings } from './run-children-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { RunCloneSettings } from './run-clone-settings';
// May contain unused imports in some cases
// @ts-ignore
@ -89,4 +92,5 @@ export interface RunNamespace {
'pull_request': PullRequestSettings | null;
'artifacts': ArtifactsSettings;
'integrations': RunIntegrationsSettings;
'children': RunChildrenSettings;
}

View file

@ -20,7 +20,8 @@
export const RunRunnableSource = {
START_REQUESTED: 'start_requested',
APPROVED: 'approved'
APPROVED: 'approved',
AUTO_APPROVED: 'auto_approved'
} as const;
export type RunRunnableSource = typeof RunRunnableSource[keyof typeof RunRunnableSource];

View file

@ -0,0 +1,22 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Server-owned approval policy. `allow_child_auto_approval` is the server half of the child auto-approval opt-in; the workflow half is `[run.children] auto_approve` in the parent workflow\'s `workflow.toml`, and a worker-started child is auto-approved only when both are enabled.
*/
export interface ServerApprovalsSettings {
'allow_child_auto_approval': boolean;
}

View file

@ -18,6 +18,9 @@
import type { ServerApiSettings } from './server-api-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { ServerApprovalsSettings } from './server-approvals-settings';
// May contain unused imports in some cases
// @ts-ignore
import type { ServerArtifactsSettings } from './server-artifacts-settings';
// May contain unused imports in some cases
// @ts-ignore
@ -53,6 +56,7 @@ export interface ServerNamespace {
'storage': ServerStorageSettings;
'artifacts': ServerArtifactsSettings;
'scheduler': ServerSchedulerSettings;
'approvals': ServerApprovalsSettings;
'logging': ServerLoggingSettings;
'integrations': ServerIntegrationsSettings;
}