Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const PRODUCT_SYMBOLS_WEBUI_STILL_NAMES: &[(&str, &str)] = &[
("AUTOMATION_PAUSE_COMMAND", "inventory"),
("AUTOMATION_RENAME_COMMAND", "inventory"),
("AUTOMATION_RESUME_COMMAND", "inventory"),
("AUTOMATION_RUN_COMMAND", "inventory"),
("CANCEL_RUN_COMMAND", "inventory"),
("CREATE_THREAD_COMMAND", "inventory"),
("EXTENSIONS_VIEW", "inventory"),
Expand Down Expand Up @@ -223,7 +224,7 @@ const PRODUCT_SYMBOLS_OPENAI_COMPAT_STILL_NAMES: &[(&str, &str)] = &[
// `product_attachment_capabilities` to `ironclaw_attachments` as
// `AttachmentCapabilities` / `attachment_capabilities` — the two symbols this
// list called out by name as that row's to own. Shrink-only.
const WEBUI_PRODUCT_SYMBOL_BASELINE: usize = 103;
const WEBUI_PRODUCT_SYMBOL_BASELINE: usize = 104;
const OPENAI_COMPAT_PRODUCT_SYMBOL_BASELINE: usize = 3;

/// Boundary vocabulary this row moved: declared in `ironclaw_product_contracts`
Expand Down
55 changes: 50 additions & 5 deletions crates/app/ironclaw_composition/src/automation/trigger_poller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,48 @@ pub(crate) use active_run_lookup::{

pub(crate) const TRIGGER_POLLER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);

#[derive(Default)]
pub(crate) struct LateBoundTriggerManualFireRunner {
runner: OnceLock<Arc<dyn ironclaw_triggers::TriggerManualFireRunner>>,
}

impl LateBoundTriggerManualFireRunner {
pub(crate) fn bind(
&self,
runner: Arc<dyn ironclaw_triggers::TriggerManualFireRunner>,
) -> Result<(), TriggerError> {
self.runner.set(runner).map_err(|_| TriggerError::Backend {
reason: "manual trigger fire runner was already bound".to_string(),
})
}
}

#[async_trait]
impl ironclaw_triggers::TriggerManualFireRunner for LateBoundTriggerManualFireRunner {
async fn run_manual_fire(
&self,
tenant_id: ironclaw_host_api::ids::TenantId,
trigger_id: ironclaw_triggers::TriggerId,
now: ironclaw_host_api::Timestamp,
) -> Result<ironclaw_triggers::TriggerManualFireOutcome, TriggerError> {
let runner = self.runner.get().ok_or_else(|| TriggerError::Backend {
reason: "manual trigger fire runner is not available".to_string(),
})?;
runner.run_manual_fire(tenant_id, trigger_id, now).await
}
}

pub(crate) struct TriggerPollerRuntimeHandle {
cancel: CancellationToken,
handle: JoinHandle<()>,
manual_fire_runner: Arc<dyn ironclaw_triggers::TriggerManualFireRunner>,
}

impl TriggerPollerRuntimeHandle {
pub(crate) fn manual_fire_runner(&self) -> Arc<dyn ironclaw_triggers::TriggerManualFireRunner> {
Arc::clone(&self.manual_fire_runner)
}

pub(crate) async fn shutdown(self, timeout: Duration) {
self.cancel.cancel();
self.join_with_timeout(timeout).await;
Expand Down Expand Up @@ -90,7 +126,7 @@ pub(crate) fn spawn_trigger_poller(
PostSubmitHookObserver::new(deps.post_submit_hook_slot, cancel.clone()),
);
let trusted_submitter = deps.trusted_submitter;
let worker = TriggerPollerWorker::new(
let worker = Arc::new(TriggerPollerWorker::new(
settings.worker.clone(),
TriggerPollerWorkerDeps {
repository: deps.repository,
Expand All @@ -100,12 +136,17 @@ pub(crate) fn spawn_trigger_poller(
active_run_lookup: deps.active_run_lookup,
fire_settlement_observer,
},
)?;
)?);
let manual_fire_runner: Arc<dyn ironclaw_triggers::TriggerManualFireRunner> = worker.clone();
let task_cancel = cancel.clone();
let handle = tokio::spawn(async move {
run_trigger_poller(worker, settings, task_cancel).await;
});
Ok(Some(TriggerPollerRuntimeHandle { cancel, handle }))
Ok(Some(TriggerPollerRuntimeHandle {
cancel,
handle,
manual_fire_runner,
}))
}

const POST_SUBMIT_HOOK_PENDING_CAPACITY: usize = 256;
Expand Down Expand Up @@ -255,7 +296,7 @@ impl TriggerFireSettlementObserver for PostSubmitHookObserver {
}

async fn run_trigger_poller(
worker: TriggerPollerWorker,
worker: Arc<TriggerPollerWorker>,
settings: TriggerPollerSettings,
cancel: CancellationToken,
) {
Expand Down Expand Up @@ -349,7 +390,11 @@ mod tests {
task_cancel.cancelled().await;
std::future::pending::<()>().await;
});
let runtime_handle = TriggerPollerRuntimeHandle { cancel, handle };
let runtime_handle = TriggerPollerRuntimeHandle {
cancel,
handle,
manual_fire_runner: Arc::new(ironclaw_triggers::MissingTriggerManualFireRunner),
};

runtime_handle.shutdown(Duration::from_millis(1)).await;
}
Expand Down
8 changes: 6 additions & 2 deletions crates/app/ironclaw_composition/src/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ use ironclaw_host_runtime::{
builtin_first_party_package,
};
use ironclaw_host_runtime::{
builtin_first_party_handlers_with_trigger_create_hook_for_process_backend,
builtin_first_party_handlers_with_trigger_services_for_process_backend,
builtin_first_party_package_for_process_backend,
};
use ironclaw_identity::projects::ProjectRepository;
Expand Down Expand Up @@ -350,6 +350,8 @@ pub(crate) struct RebornRuntimeStores {
pub(crate) processes: ProcessRuntimeSystem,
pub(crate) thread_service: Arc<dyn SessionThreadService>,
pub(crate) trigger_repository: Arc<dyn TriggerRepository>,
pub(crate) trigger_manual_fire_runner:
Arc<crate::automation::trigger_poller::LateBoundTriggerManualFireRunner>,
pub(crate) trigger_create_hook: Arc<TriggerCreatorPairingHook>,
pub(crate) resource_governor: Arc<dyn ResourceGovernor>,
pub(crate) budget_gate_store: Arc<dyn BudgetGateStorePort>,
Expand Down Expand Up @@ -1214,12 +1216,14 @@ fn production_first_party_registry_with_trigger_create_hook(
trigger_repository: Arc<dyn TriggerRepository>,
trigger_create_hook: Arc<dyn TriggerCreateHook>,
active_run_lookup: Arc<dyn TriggerActiveRunLookup>,
manual_fire_runner: Arc<dyn ironclaw_triggers::TriggerManualFireRunner>,
process_backend: ProcessBackendKind,
) -> Result<FirstPartyCapabilityRegistry, RebornBuildError> {
builtin_first_party_handlers_with_trigger_create_hook_for_process_backend(
builtin_first_party_handlers_with_trigger_services_for_process_backend(
trigger_repository,
trigger_create_hook,
active_run_lookup,
manual_fire_runner,
process_backend,
)
.map_err(|error| RebornBuildError::InvalidConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,10 +600,13 @@ pub(super) async fn build_backend_production(
>,
>),
);
let trigger_manual_fire_runner =
Arc::new(crate::automation::trigger_poller::LateBoundTriggerManualFireRunner::default());
let mut first_party_registry = production_first_party_registry_with_trigger_create_hook(
Arc::clone(&trigger_repository),
Arc::clone(&trigger_create_hook) as Arc<dyn TriggerCreateHook>,
trigger_active_run_lookup,
trigger_manual_fire_runner.clone(),
process_backend,
)?;
if let (Some(package), Some(handler)) = (
Expand Down Expand Up @@ -1439,6 +1442,7 @@ pub(super) async fn build_backend_production(
processes,
thread_service,
trigger_repository: Arc::clone(&trigger_repository),
trigger_manual_fire_runner,
trigger_create_hook,
resource_governor: production_resource_governor,
budget_gate_store,
Expand Down
1 change: 1 addition & 0 deletions crates/app/ironclaw_composition/src/product_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ pub(crate) fn build_product_surface_with_channel_connection(
);
api = api.with_automation_product_service(Arc::new(
RebornAutomationProductService::new(backing.repository, active_run_lookup)
.with_manual_fire_runner(Arc::clone(&runtime.trigger_manual_fire_runner))
.with_scheduler_enabled(runtime.readiness.workers.trigger_poller),
));
// First-class projects + membership (ACL). Built once per runtime over the
Expand Down
11 changes: 11 additions & 0 deletions crates/app/ironclaw_composition/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ pub struct RebornRuntime {
Arc<dyn ironclaw_product_contracts::project_service::ProjectService>,
pub(crate) diagnostic_store: Arc<dyn ironclaw_assistant::inspector_store::DiagnosticStorePort>,
pub(crate) trigger_repository: Arc<dyn ironclaw_triggers::TriggerRepository>,
pub(crate) trigger_manual_fire_runner: Arc<dyn ironclaw_triggers::TriggerManualFireRunner>,
#[cfg(any(test, feature = "test-support"))]
#[allow(
dead_code,
Expand Down Expand Up @@ -3496,6 +3497,7 @@ pub(crate) async fn build_runtime_with_resource_governor(
outbound_preferences_facade.clone(),
trajectory_observer,
Some(tool_diagnostic_sink),
trigger_poller.enabled,
)
.ok_or(RebornRuntimeError::HostRuntimeUnavailable)?;
(
Expand Down Expand Up @@ -4315,6 +4317,14 @@ pub(crate) async fn build_runtime_with_resource_governor(
.map_err(|error| RebornRuntimeError::InvalidArgument {
reason: format!("trigger poller could not be started: {error}"),
})?;
if let Some(handle) = trigger_poller_handle.as_ref() {
services
.trigger_manual_fire_runner
.bind(handle.manual_fire_runner())
.map_err(|error| RebornRuntimeError::InvalidArgument {
reason: format!("manual trigger fire runner could not be bound: {error}"),
})?;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
} else {
trigger_poller_handle = None;
runtime_post_submit_hook_slot = None;
Expand Down Expand Up @@ -4501,6 +4511,7 @@ pub(crate) async fn build_runtime_with_resource_governor(
project_service,
diagnostic_store,
trigger_repository: trigger_repository.clone(),
trigger_manual_fire_runner: services.trigger_manual_fire_runner.clone(),
#[cfg(any(test, feature = "test-support"))]
trigger_process_lifecycle_source: Arc::clone(&services.trigger_process_lifecycle_source),
broadcast_budget_event_sink,
Expand Down
11 changes: 10 additions & 1 deletion crates/app/ironclaw_composition/src/runtime/capability_host.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Capability-host adapters and assembly shared by every runtime profile.

use std::{
collections::{BTreeMap, HashMap, VecDeque},
collections::{BTreeMap, HashMap, HashSet, VecDeque},
sync::{Arc, Mutex as StdMutex},
};

Expand Down Expand Up @@ -120,6 +120,7 @@ pub(super) fn capability_wiring(
outbound_preferences_service: Option<Arc<dyn OutboundPreferencesProductService>>,
trajectory_observer: Option<Arc<dyn crate::RebornTrajectoryObserver>>,
tool_diagnostic_sink: Option<Arc<dyn HostManagedPromptDiagnosticSink>>,
trigger_poller_enabled: bool,
) -> Option<CapabilityPortWiring> {
let runtime = services.host_runtime.clone();
let workspace_mounts = services.workspace_mounts.clone();
Expand Down Expand Up @@ -172,6 +173,11 @@ pub(super) fn capability_wiring(
// run-scoped external-tool state.
let external_tool_catalog: Arc<dyn ExternalToolCatalog> =
services.external_tool_catalog.clone();
let unavailable_capability_ids = if trigger_poller_enabled {
HashSet::new()
} else {
HashSet::from([CapabilityId::new(ironclaw_host_runtime::TRIGGER_RUN_CAPABILITY_ID).ok()?])
};
// Wire the durable gate-record and host-private replay-payload stores over
// the composition-owned scoped filesystem (same backend + per-user mount view
// as every other durable store; `extension_filesystem` is the shared composite
Expand Down Expand Up @@ -211,6 +217,7 @@ pub(super) fn capability_wiring(
gate_record_store,
replay_payload_store,
external_tool_catalog,
unavailable_capability_ids,
});
Some(CapabilityPortWiring {
capability_factory,
Expand Down Expand Up @@ -254,6 +261,7 @@ struct RefreshingLoopCapabilityPortFactory {
/// all runs in this runtime so a parked external-tool call and its later
/// client-submitted output (across a pause/resume) hit the same store.
external_tool_catalog: Arc<dyn ExternalToolCatalog>,
unavailable_capability_ids: HashSet<CapabilityId>,
}

/// Make skill files readable by the ordinary filesystem tools, read-only.
Expand Down Expand Up @@ -356,6 +364,7 @@ impl LoopCapabilityPortFactory for RefreshingLoopCapabilityPortFactory {
capability_execution_mount_overrides: HashMap::new(),
additional_provider_trust: BTreeMap::new(),
capability_id_filter: None,
unavailable_capability_ids: self.unavailable_capability_ids.clone(),
additional_capability_grants: Vec::new(),
})
.await
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ pub(crate) struct RefreshingCapabilityPortConfig {
/// call site, `capability_host.rs`'s `create_capability_port`); `Some(set)`
/// keeps exactly `set`, including `Some(empty)` = zero grants.
pub(super) capability_id_filter: Option<HashSet<CapabilityId>>,
/// Runtime-unavailable builtins omitted from every refreshed grant set.
/// Unlike `capability_id_filter`, this production seam is a deny-list and
/// therefore preserves dynamically discovered extension capabilities.
pub(super) unavailable_capability_ids: HashSet<CapabilityId>,
/// Synthetic grants for capability ids that neither the static builtin
/// policy nor `extension_surface_source` produces (ad-hoc test-only
/// `HostRuntime` backends). Applied in `build_inner` before
Expand Down Expand Up @@ -130,6 +134,7 @@ pub(crate) async fn create_refreshing_capability_port(
capability_execution_mount_overrides: config.capability_execution_mount_overrides,
additional_provider_trust: config.additional_provider_trust,
capability_id_filter: config.capability_id_filter,
unavailable_capability_ids: config.unavailable_capability_ids,
additional_capability_grants: config.additional_capability_grants,
current: StdMutex::new(None),
refresh_lock: AsyncMutex::new(()),
Expand Down Expand Up @@ -170,6 +175,7 @@ struct RefreshingCapabilityPort {
capability_execution_mount_overrides: HashMap<CapabilityId, MountView>,
additional_provider_trust: BTreeMap<ExtensionId, TrustDecision>,
capability_id_filter: Option<HashSet<CapabilityId>>,
unavailable_capability_ids: HashSet<CapabilityId>,
additional_capability_grants: Vec<ironclaw_host_api::capability::CapabilityGrant>,
current: StdMutex<Option<Arc<dyn LoopCapabilityPort>>>,
refresh_lock: AsyncMutex<()>,
Expand Down Expand Up @@ -248,6 +254,11 @@ impl RefreshingCapabilityPort {
.grants
.retain(|grant| filter.contains(&grant.capability));
}
visible_request
.context
.grants
.grants
.retain(|grant| !self.unavailable_capability_ids.contains(&grant.capability));
// Test-support-only extra provider-trust entries (empty in production,
// see the config field doc-comment): merge after the canonical helper
// has built the base provider-trust map, so the production helper
Expand Down Expand Up @@ -637,6 +648,7 @@ pub(crate) async fn create_refreshing_capability_port_for_test(
capability_execution_mount_overrides,
additional_provider_trust,
capability_id_filter,
unavailable_capability_ids: HashSet::new(),
additional_capability_grants,
})
.await
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ async fn standalone_yolo_shell_translates_workspace_workdir_without_scoped_mount
external_tool_catalog: std::sync::Arc::new(
ironclaw_turns::InMemoryExternalToolCatalog::new(),
),
unavailable_capability_ids: std::collections::HashSet::new(),
};
let run_context = run_context("shell-workdir").await;
// Turn on the global auto-approve switch for this run's actor scope so the
Expand Down
Loading
Loading