feat(sdk,plugins): API publique d'accès fichiers/workspace + analyse structure (#124,#129)
This commit is contained in:
@ -397,6 +397,21 @@ pub fn run() {
|
||||
plugins::plugin_set_enabled,
|
||||
plugins::plugin_uninstall,
|
||||
plugins::plugin_list_runtime_contributions,
|
||||
plugins::plugin_workspace_read_text,
|
||||
plugins::plugin_workspace_read_binary,
|
||||
plugins::plugin_workspace_write_text,
|
||||
plugins::plugin_workspace_write_binary,
|
||||
plugins::plugin_workspace_list_dir,
|
||||
plugins::plugin_workspace_stat,
|
||||
plugins::plugin_query_project_structure,
|
||||
plugins::plugin_config_read_document,
|
||||
plugins::plugin_config_update_document,
|
||||
plugins::plugin_task_run_command,
|
||||
plugins::plugin_task_get_status,
|
||||
plugins::plugin_toolchain_diagnose,
|
||||
plugins::plugin_events_subscribe,
|
||||
plugins::plugin_events_poll,
|
||||
plugins::plugin_events_unsubscribe,
|
||||
plugins::plugin_open_plugins_folder,
|
||||
commands::get_server_exposure_settings,
|
||||
commands::save_server_exposure_settings,
|
||||
@ -415,6 +430,28 @@ pub fn run() {
|
||||
.expect("error while running IdeA Tauri application");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn plugin_workspace_invoke_handler<R: tauri::Runtime>(
|
||||
) -> impl Fn(tauri::ipc::Invoke<R>) -> bool + Send + Sync + 'static {
|
||||
tauri::generate_handler![
|
||||
plugins::plugin_workspace_read_text,
|
||||
plugins::plugin_workspace_read_binary,
|
||||
plugins::plugin_workspace_write_text,
|
||||
plugins::plugin_workspace_write_binary,
|
||||
plugins::plugin_workspace_list_dir,
|
||||
plugins::plugin_workspace_stat,
|
||||
plugins::plugin_query_project_structure,
|
||||
plugins::plugin_config_read_document,
|
||||
plugins::plugin_config_update_document,
|
||||
plugins::plugin_task_run_command,
|
||||
plugins::plugin_task_get_status,
|
||||
plugins::plugin_toolchain_diagnose,
|
||||
plugins::plugin_events_subscribe,
|
||||
plugins::plugin_events_poll,
|
||||
plugins::plugin_events_unsubscribe,
|
||||
]
|
||||
}
|
||||
|
||||
async fn app_exit_work_guard_state(
|
||||
handle: &tauri::AppHandle,
|
||||
) -> Result<application::AppExitWorkGuardState, AppError> {
|
||||
@ -713,6 +750,7 @@ fn persisted_monitor_is_available(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::plugin_workspace_invoke_handler;
|
||||
use super::{
|
||||
apply_main_close_decision, confirm_next_main_window_close, consume_exit_guard_confirmation,
|
||||
decide_main_close_action, persisted_view_identity_from_label, persisted_window_identity,
|
||||
@ -720,7 +758,10 @@ mod tests {
|
||||
};
|
||||
use super::{should_close_with_main_window, PersistedWindowKind};
|
||||
use application::AppExitWorkGuardState;
|
||||
use serde_json::json;
|
||||
use std::cell::Cell;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::test::{get_ipc_response, mock_builder, mock_context, noop_assets, INVOKE_KEY};
|
||||
|
||||
#[test]
|
||||
fn main_close_without_work_allows_shutdown_without_preventing_close() {
|
||||
@ -880,4 +921,289 @@ mod tests {
|
||||
);
|
||||
assert!(persisted_window_identity("view-tickets-not-a-project").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_workspace_commands_are_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-workspace-commands");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(124).to_string();
|
||||
|
||||
for command in [
|
||||
"plugin_workspace_read_text",
|
||||
"plugin_workspace_read_binary",
|
||||
"plugin_workspace_list_dir",
|
||||
"plugin_workspace_stat",
|
||||
"plugin_query_project_structure",
|
||||
] {
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
command,
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project.clone(),
|
||||
"path": "src/main.rs",
|
||||
"maxDepth": 2,
|
||||
"maxEntries": 10
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND", "{command}");
|
||||
}
|
||||
|
||||
for (command, input) in [
|
||||
(
|
||||
"plugin_workspace_write_text",
|
||||
json!({
|
||||
"projectId": missing_project.clone(),
|
||||
"path": "generated.txt",
|
||||
"content": "hello\n"
|
||||
}),
|
||||
),
|
||||
(
|
||||
"plugin_workspace_write_binary",
|
||||
json!({
|
||||
"projectId": missing_project.clone(),
|
||||
"path": "generated.bin",
|
||||
"bytes": [1, 2, 3]
|
||||
}),
|
||||
),
|
||||
] {
|
||||
let err = invoke_plugin_command(&webview, command, json!({ "input": input }))
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND", "{command}");
|
||||
}
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_config_document_commands_are_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-config-document-commands");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(130).to_string();
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_config_read_document",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project.clone(),
|
||||
"path": "config/settings.json",
|
||||
"format": "json"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_config_update_document",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project,
|
||||
"path": "config/settings.json",
|
||||
"format": "json",
|
||||
"mode": "mergePatch",
|
||||
"value": {"enabled": true}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_command_task_commands_are_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-task-commands");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(125).to_string();
|
||||
let owner = uuid::Uuid::from_u128(126).to_string();
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_task_run_command",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project,
|
||||
"ownerAgentId": owner,
|
||||
"label": "cargo test",
|
||||
"command": "cargo",
|
||||
"args": ["test"],
|
||||
"cwd": ".",
|
||||
"env": [["RUST_LOG", "debug"]],
|
||||
"recordOnly": true
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
let task_id = uuid::Uuid::from_u128(127).to_string();
|
||||
let value = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_task_get_status",
|
||||
json!({
|
||||
"input": {
|
||||
"taskId": task_id
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect("unknown task is a successful empty status");
|
||||
assert_eq!(value, serde_json::Value::Null);
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_toolchain_diagnostic_command_is_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-toolchain-diagnostic-command");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(126).to_string();
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_toolchain_diagnose",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project,
|
||||
"cwd": ".",
|
||||
"tools": [{
|
||||
"id": "rust",
|
||||
"executable": "cargo",
|
||||
"versionArgs": ["--version"],
|
||||
"required": true
|
||||
}],
|
||||
"env": [{
|
||||
"name": "RUSTUP_HOME",
|
||||
"required": false
|
||||
}],
|
||||
"files": [{
|
||||
"path": "Cargo.toml",
|
||||
"required": true,
|
||||
"kind": "file"
|
||||
}]
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_event_commands_are_registered_in_tauri_invoke_handler() {
|
||||
let app_data = test_app_data_dir("plugin-event-commands");
|
||||
let app = mock_builder()
|
||||
.manage(crate::state::AppState::build(app_data.clone()))
|
||||
.invoke_handler(plugin_workspace_invoke_handler())
|
||||
.build(mock_context(noop_assets()))
|
||||
.expect("mock app builds");
|
||||
let webview = tauri::WebviewWindowBuilder::new(&app, "main", Default::default())
|
||||
.build()
|
||||
.expect("mock webview builds");
|
||||
let missing_project = uuid::Uuid::from_u128(127).to_string();
|
||||
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_events_subscribe",
|
||||
json!({
|
||||
"input": {
|
||||
"projectId": missing_project,
|
||||
"eventTypes": ["workspaceFileChanged", "backgroundTaskChanged"],
|
||||
"capacity": 10
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("missing project must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
let subscription_id = uuid::Uuid::from_u128(128).to_string();
|
||||
let err = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_events_poll",
|
||||
json!({
|
||||
"input": {
|
||||
"subscriptionId": subscription_id.clone(),
|
||||
"maxEvents": 10
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect_err("unknown subscription must surface through the registered command");
|
||||
assert_eq!(err["code"], "NOT_FOUND");
|
||||
|
||||
let disposed = invoke_plugin_command(
|
||||
&webview,
|
||||
"plugin_events_unsubscribe",
|
||||
json!({
|
||||
"input": {
|
||||
"subscriptionId": subscription_id
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect("unsubscribe is idempotent");
|
||||
assert_eq!(disposed["retention"], "disposed");
|
||||
|
||||
std::fs::remove_dir_all(app_data).ok();
|
||||
}
|
||||
|
||||
fn invoke_plugin_command<W: AsRef<tauri::Webview<tauri::test::MockRuntime>>>(
|
||||
webview: &W,
|
||||
command: &str,
|
||||
body: serde_json::Value,
|
||||
) -> Result<serde_json::Value, serde_json::Value> {
|
||||
get_ipc_response(
|
||||
webview,
|
||||
tauri::webview::InvokeRequest {
|
||||
cmd: command.to_owned(),
|
||||
callback: tauri::ipc::CallbackFn(0),
|
||||
error: tauri::ipc::CallbackFn(1),
|
||||
url: "tauri://localhost".parse().unwrap(),
|
||||
body: tauri::ipc::InvokeBody::Json(body),
|
||||
headers: Default::default(),
|
||||
invoke_key: INVOKE_KEY.to_owned(),
|
||||
},
|
||||
)
|
||||
.map(|body| body.deserialize::<serde_json::Value>().unwrap())
|
||||
}
|
||||
|
||||
fn test_app_data_dir(label: &str) -> std::path::PathBuf {
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
std::env::temp_dir().join(format!("idea-{label}-{}-{nanos}", std::process::id()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,8 +5,16 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use application::{ReviewPluginPackageInput, SetPluginEnabledInput, UninstallPluginInput};
|
||||
use backend::dto::{
|
||||
ErrorDto, PluginAdminDto, PluginInstallResultDto, PluginReviewDto,
|
||||
PluginRuntimeContributionCatalogDto, PluginUninstallResultDto, ReviewPluginPackageDto,
|
||||
ErrorDto, PluginAdminDto, PluginConfigDocumentDto, PluginConfigDocumentReadDto,
|
||||
PluginConfigDocumentUpdateDto, PluginConfigDocumentWriteResultDto, PluginEventBatchDto,
|
||||
PluginEventPollDto, PluginEventSubscribeDto, PluginEventSubscriptionDto,
|
||||
PluginEventUnsubscribeDto, PluginInstallResultDto, PluginProjectStructureDto,
|
||||
PluginProjectStructureQueryDto, PluginReviewDto, PluginRunCommandDto,
|
||||
PluginRuntimeContributionCatalogDto, PluginTaskDto, PluginTaskStatusDto,
|
||||
PluginToolchainDiagnosticDto, PluginToolchainDiagnosticRequestDto, PluginUninstallResultDto,
|
||||
PluginWorkspaceBinaryFileDto, PluginWorkspaceDirectoryListingDto, PluginWorkspacePathDto,
|
||||
PluginWorkspaceStatDto, PluginWorkspaceTextFileDto, PluginWorkspaceWriteBinaryDto,
|
||||
PluginWorkspaceWriteTextDto, ReviewPluginPackageDto,
|
||||
};
|
||||
use domain::ports::{PluginManifestValidator, PluginPackageStore, PluginRegistryStore};
|
||||
use domain::{PluginId, RelativePath};
|
||||
@ -156,6 +164,198 @@ pub async fn plugin_list_runtime_contributions(
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Reads a UTF-8 workspace file for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_read_text(
|
||||
input: PluginWorkspacePathDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginWorkspaceTextFileDto, ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.read_text(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Reads a binary workspace file for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_read_binary(
|
||||
input: PluginWorkspacePathDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginWorkspaceBinaryFileDto, ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.read_binary(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Writes a UTF-8 workspace file for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_write_text(
|
||||
input: PluginWorkspaceWriteTextDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.write_text(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Writes a binary workspace file for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_write_binary(
|
||||
input: PluginWorkspaceWriteBinaryDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.write_binary(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Lists a workspace directory for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_list_dir(
|
||||
input: PluginWorkspacePathDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginWorkspaceDirectoryListingDto, ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.list_dir(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Stats a workspace path for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_workspace_stat(
|
||||
input: PluginWorkspacePathDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginWorkspaceStatDto, ErrorDto> {
|
||||
state
|
||||
.plugin_workspace_access
|
||||
.stat(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Queries a bounded generic project structure for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_query_project_structure(
|
||||
input: PluginProjectStructureQueryDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginProjectStructureDto, ErrorDto> {
|
||||
state
|
||||
.query_project_structure
|
||||
.execute(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Reads a structured configuration document for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_config_read_document(
|
||||
input: PluginConfigDocumentReadDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginConfigDocumentDto, ErrorDto> {
|
||||
state
|
||||
.plugin_config_documents
|
||||
.read(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Updates a structured configuration document for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_config_update_document(
|
||||
input: PluginConfigDocumentUpdateDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginConfigDocumentWriteResultDto, ErrorDto> {
|
||||
state
|
||||
.plugin_config_documents
|
||||
.update(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Launches a command-backed background task for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_task_run_command(
|
||||
input: PluginRunCommandDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginTaskDto, ErrorDto> {
|
||||
state
|
||||
.plugin_command_tasks
|
||||
.run_command(input.into())
|
||||
.await
|
||||
.map(PluginTaskDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Reads one command task status for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_task_get_status(
|
||||
input: PluginTaskStatusDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Option<PluginTaskDto>, ErrorDto> {
|
||||
state
|
||||
.plugin_command_tasks
|
||||
.get_status(input.into())
|
||||
.await
|
||||
.map(|task| task.map(PluginTaskDto::from))
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Diagnoses generic external toolchain prerequisites for the public plugin API.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_toolchain_diagnose(
|
||||
input: PluginToolchainDiagnosticRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginToolchainDiagnosticDto, ErrorDto> {
|
||||
state
|
||||
.plugin_toolchain_diagnostics
|
||||
.diagnose(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Subscribes to stable public plugin events.
|
||||
#[tauri::command]
|
||||
pub async fn plugin_events_subscribe(
|
||||
input: PluginEventSubscribeDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginEventSubscriptionDto, ErrorDto> {
|
||||
state
|
||||
.plugin_event_subscriptions
|
||||
.subscribe(input.into())
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Drains retained public plugin events for one subscription.
|
||||
#[tauri::command]
|
||||
pub fn plugin_events_poll(
|
||||
input: PluginEventPollDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<PluginEventBatchDto, ErrorDto> {
|
||||
state
|
||||
.plugin_event_subscriptions
|
||||
.poll(input.into())
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// Disposes a public plugin event subscription.
|
||||
#[tauri::command]
|
||||
pub fn plugin_events_unsubscribe(
|
||||
input: PluginEventUnsubscribeDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> PluginEventSubscriptionDto {
|
||||
state.plugin_event_subscriptions.unsubscribe(input.into())
|
||||
}
|
||||
|
||||
/// Opens the plugin store folder, or one plugin folder when an id is provided.
|
||||
#[tauri::command]
|
||||
pub fn plugin_open_plugins_folder(
|
||||
|
||||
@ -17,11 +17,12 @@ use std::sync::Arc;
|
||||
|
||||
use domain::ports::{
|
||||
BackgroundTaskPortError, BackgroundTaskRunner, BackgroundTaskSpec, BackgroundTaskStore, Clock,
|
||||
IdGenerator, SpawnSpec,
|
||||
EventBus, IdGenerator, SpawnSpec,
|
||||
};
|
||||
use domain::{
|
||||
AgentId, BackgroundTask, BackgroundTaskKind, BackgroundTaskRendezvousLink,
|
||||
BackgroundTaskResult, BackgroundTaskState, BackgroundTaskWakePolicy, ProjectId, TaskId,
|
||||
BackgroundTaskResult, BackgroundTaskState, BackgroundTaskWakePolicy, DomainEvent, ProjectId,
|
||||
TaskId,
|
||||
};
|
||||
|
||||
use crate::error::AppError;
|
||||
@ -71,6 +72,7 @@ pub struct SpawnBackgroundCommand {
|
||||
runner: Arc<dyn BackgroundTaskRunner>,
|
||||
clock: Arc<dyn Clock>,
|
||||
ids: Arc<dyn IdGenerator>,
|
||||
events: Option<Arc<dyn EventBus>>,
|
||||
}
|
||||
|
||||
impl SpawnBackgroundCommand {
|
||||
@ -87,9 +89,17 @@ impl SpawnBackgroundCommand {
|
||||
runner,
|
||||
clock,
|
||||
ids,
|
||||
events: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches the public event stream publisher.
|
||||
#[must_use]
|
||||
pub fn with_events(mut self, events: Arc<dyn EventBus>) -> Self {
|
||||
self.events = Some(events);
|
||||
self
|
||||
}
|
||||
|
||||
/// Allocates a task id, persists it (`Queued`→`Running`) and spawns it.
|
||||
///
|
||||
/// # Errors
|
||||
@ -145,6 +155,19 @@ impl SpawnBackgroundCommand {
|
||||
.transition(BackgroundTaskState::Running, now)
|
||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||
self.store.save(&running).await.map_err(map_port_err)?;
|
||||
if let Some(events) = &self.events {
|
||||
events.publish(DomainEvent::BackgroundTaskStarted {
|
||||
project_id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
});
|
||||
events.publish(DomainEvent::BackgroundTaskStateChanged {
|
||||
project_id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
state: BackgroundTaskState::Running,
|
||||
});
|
||||
}
|
||||
|
||||
let spec = BackgroundTaskSpec {
|
||||
task_id,
|
||||
@ -168,6 +191,14 @@ impl SpawnBackgroundCommand {
|
||||
}) {
|
||||
let _ = self.store.save(&failed).await;
|
||||
}
|
||||
if let Some(events) = &self.events {
|
||||
events.publish(DomainEvent::BackgroundTaskFailed {
|
||||
project_id,
|
||||
task_id,
|
||||
owner_agent_id,
|
||||
rendezvous: None,
|
||||
});
|
||||
}
|
||||
return Err(map_port_err(err));
|
||||
}
|
||||
|
||||
|
||||
@ -155,10 +155,23 @@ pub use permission::{
|
||||
};
|
||||
pub use plugin::{
|
||||
InstallPluginFromArchive, InstallPluginFromDirectory, JsonPluginManifestValidator,
|
||||
ListPluginRuntimeContributions, ListPlugins, PluginAdmin, PluginContributionSummary,
|
||||
PluginInstallResult, PluginReview, PluginRuntimeCatalog, PluginRuntimePlugin,
|
||||
ReconcilePluginMcpServers, ReviewPluginPackage, ReviewPluginPackageInput, SetPluginEnabled,
|
||||
SetPluginEnabledInput, UninstallPlugin, UninstallPluginInput, UninstallPluginResult,
|
||||
ListPluginRuntimeContributions, ListPlugins, PluginAdmin, PluginCommandTasks,
|
||||
PluginConfigDocument, PluginConfigDocumentReadInput, PluginConfigDocumentUpdateInput,
|
||||
PluginConfigDocumentWriteResult, PluginConfigDocuments, PluginContributionSummary,
|
||||
PluginDiagnosticMessage, PluginEnvDiagnostic, PluginEnvRequirement, PluginEventBatch,
|
||||
PluginEventPollInput, PluginEventSubscribeInput, PluginEventSubscription,
|
||||
PluginEventSubscriptions, PluginEventUnsubscribeInput, PluginFileDiagnostic,
|
||||
PluginFileRequirement, PluginInstallResult, PluginPublicEvent, PluginReview,
|
||||
PluginRunCommandInput, PluginRuntimeCatalog, PluginRuntimePlugin, PluginTaskStatusInput,
|
||||
PluginToolDiagnostic, PluginToolRequirement, PluginToolchainDiagnostic,
|
||||
PluginToolchainDiagnosticInput, PluginToolchainDiagnostics, PluginWorkspaceAccess,
|
||||
PluginWorkspaceBinaryFile, PluginWorkspaceDirEntry, PluginWorkspaceDirectoryListing,
|
||||
PluginWorkspacePathInput, PluginWorkspaceStat, PluginWorkspaceTextFile,
|
||||
PluginWorkspaceWriteBinaryInput, PluginWorkspaceWriteTextInput, ProjectConvention,
|
||||
ProjectModule, ProjectStructureEntry, ProjectStructureQuery, QueryProjectStructure,
|
||||
QueryProjectStructureInput, ReconcilePluginMcpServers, ReviewPluginPackage,
|
||||
ReviewPluginPackageInput, SetPluginEnabled, SetPluginEnabledInput, UninstallPlugin,
|
||||
UninstallPluginInput, UninstallPluginResult,
|
||||
};
|
||||
pub use project::{
|
||||
CloseProject, CloseProjectInput, CloseProjectOutput, CloseTab, CloseTabInput, CreateProject,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -7,6 +7,7 @@
|
||||
//! JSON convention already used in the domain (`agents.json` etc.).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use application::{
|
||||
AgentBackgroundTaskState, AgentTicketState, AppError, AppExitWorkGuardDetail,
|
||||
@ -273,6 +274,408 @@ impl From<application::PluginRuntimePlugin> for PluginRuntimePluginDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin workspace path request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginWorkspacePathDto {
|
||||
/// Project id.
|
||||
pub project_id: String,
|
||||
/// Relative path under the project root.
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
impl From<PluginWorkspacePathDto> for application::PluginWorkspacePathInput {
|
||||
fn from(value: PluginWorkspacePathDto) -> Self {
|
||||
Self {
|
||||
project_id: value.project_id,
|
||||
path: value.path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin workspace text write request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginWorkspaceWriteTextDto {
|
||||
/// Project id.
|
||||
pub project_id: String,
|
||||
/// Relative path under the project root.
|
||||
pub path: String,
|
||||
/// UTF-8 content.
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
impl From<PluginWorkspaceWriteTextDto> for application::PluginWorkspaceWriteTextInput {
|
||||
fn from(value: PluginWorkspaceWriteTextDto) -> Self {
|
||||
Self {
|
||||
project_id: value.project_id,
|
||||
path: value.path,
|
||||
content: value.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin workspace binary write request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginWorkspaceWriteBinaryDto {
|
||||
/// Project id.
|
||||
pub project_id: String,
|
||||
/// Relative path under the project root.
|
||||
pub path: String,
|
||||
/// Raw bytes.
|
||||
pub bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl From<PluginWorkspaceWriteBinaryDto> for application::PluginWorkspaceWriteBinaryInput {
|
||||
fn from(value: PluginWorkspaceWriteBinaryDto) -> Self {
|
||||
Self {
|
||||
project_id: value.project_id,
|
||||
path: value.path,
|
||||
bytes: value.bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin structured config document read request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginConfigDocumentReadDto {
|
||||
/// Project id.
|
||||
pub project_id: String,
|
||||
/// Relative path under the project root.
|
||||
pub path: String,
|
||||
/// Optional explicit format. Omitted means inferred from extension.
|
||||
#[serde(default)]
|
||||
pub format: Option<String>,
|
||||
}
|
||||
|
||||
impl From<PluginConfigDocumentReadDto> for application::PluginConfigDocumentReadInput {
|
||||
fn from(value: PluginConfigDocumentReadDto) -> Self {
|
||||
Self {
|
||||
project_id: value.project_id,
|
||||
path: value.path,
|
||||
format: value.format,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin structured config document update request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginConfigDocumentUpdateDto {
|
||||
/// Project id.
|
||||
pub project_id: String,
|
||||
/// Relative path under the project root.
|
||||
pub path: String,
|
||||
/// Optional explicit format. Omitted means inferred from extension.
|
||||
#[serde(default)]
|
||||
pub format: Option<String>,
|
||||
/// Update mode: `mergePatch` (default) or `replace`.
|
||||
#[serde(default)]
|
||||
pub mode: Option<String>,
|
||||
/// JSON replacement or merge patch.
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
impl From<PluginConfigDocumentUpdateDto> for application::PluginConfigDocumentUpdateInput {
|
||||
fn from(value: PluginConfigDocumentUpdateDto) -> Self {
|
||||
Self {
|
||||
project_id: value.project_id,
|
||||
path: value.path,
|
||||
format: value.format,
|
||||
mode: value.mode,
|
||||
value: value.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin project structure query request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginProjectStructureQueryDto {
|
||||
/// Project id.
|
||||
pub project_id: String,
|
||||
/// Optional relative root path.
|
||||
#[serde(default)]
|
||||
pub path: Option<String>,
|
||||
/// Optional traversal depth.
|
||||
#[serde(default)]
|
||||
pub max_depth: Option<u8>,
|
||||
/// Optional entry cap.
|
||||
#[serde(default)]
|
||||
pub max_entries: Option<usize>,
|
||||
}
|
||||
|
||||
impl From<PluginProjectStructureQueryDto> for application::QueryProjectStructureInput {
|
||||
fn from(value: PluginProjectStructureQueryDto) -> Self {
|
||||
Self {
|
||||
project_id: value.project_id,
|
||||
path: value.path,
|
||||
max_depth: value.max_depth,
|
||||
max_entries: value.max_entries,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin command-task launch request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginRunCommandDto {
|
||||
/// Owning project id.
|
||||
pub project_id: String,
|
||||
/// Agent id used for Work correlation and completion wake delivery.
|
||||
pub owner_agent_id: String,
|
||||
/// Human-facing task label.
|
||||
pub label: String,
|
||||
/// Executable to run.
|
||||
pub command: String,
|
||||
/// Arguments passed without shell parsing.
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
/// Relative working directory under project root. Empty/omitted means root.
|
||||
#[serde(default)]
|
||||
pub cwd: Option<String>,
|
||||
/// Extra environment variables.
|
||||
#[serde(default)]
|
||||
pub env: Vec<(String, String)>,
|
||||
/// When true, completion is only recorded; otherwise the owner is woken.
|
||||
#[serde(default)]
|
||||
pub record_only: bool,
|
||||
/// Optional absolute deadline, epoch milliseconds.
|
||||
#[serde(default)]
|
||||
pub deadline_ms: Option<u64>,
|
||||
}
|
||||
|
||||
impl From<PluginRunCommandDto> for application::PluginRunCommandInput {
|
||||
fn from(value: PluginRunCommandDto) -> Self {
|
||||
Self {
|
||||
project_id: value.project_id,
|
||||
owner_agent_id: value.owner_agent_id,
|
||||
label: value.label,
|
||||
command: value.command,
|
||||
args: value.args,
|
||||
cwd: value.cwd,
|
||||
env: value.env,
|
||||
record_only: value.record_only,
|
||||
deadline_ms: value.deadline_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin task status request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginTaskStatusDto {
|
||||
/// Task id to read.
|
||||
pub task_id: String,
|
||||
}
|
||||
|
||||
impl From<PluginTaskStatusDto> for application::PluginTaskStatusInput {
|
||||
fn from(value: PluginTaskStatusDto) -> Self {
|
||||
Self {
|
||||
task_id: value.task_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin command-task status/output DTO.
|
||||
pub type PluginTaskDto = BackgroundTaskDto;
|
||||
|
||||
/// Public plugin external-toolchain diagnostic request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginToolchainDiagnosticRequestDto {
|
||||
/// Owning project id.
|
||||
pub project_id: String,
|
||||
/// Relative working directory under project root.
|
||||
#[serde(default)]
|
||||
pub cwd: Option<String>,
|
||||
/// Executable probes to run.
|
||||
#[serde(default)]
|
||||
pub tools: Vec<PluginToolRequirementDto>,
|
||||
/// Environment variable prerequisites.
|
||||
#[serde(default)]
|
||||
pub env: Vec<PluginEnvRequirementDto>,
|
||||
/// Workspace file prerequisites.
|
||||
#[serde(default)]
|
||||
pub files: Vec<PluginFileRequirementDto>,
|
||||
}
|
||||
|
||||
impl From<PluginToolchainDiagnosticRequestDto> for application::PluginToolchainDiagnosticInput {
|
||||
fn from(value: PluginToolchainDiagnosticRequestDto) -> Self {
|
||||
Self {
|
||||
project_id: value.project_id,
|
||||
cwd: value.cwd,
|
||||
tools: value.tools.into_iter().map(Into::into).collect(),
|
||||
env: value.env.into_iter().map(Into::into).collect(),
|
||||
files: value.files.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin executable probe DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginToolRequirementDto {
|
||||
/// Stable requirement id.
|
||||
pub id: String,
|
||||
/// Executable name or path.
|
||||
pub executable: String,
|
||||
/// Version/diagnostic arguments.
|
||||
#[serde(default)]
|
||||
pub version_args: Vec<String>,
|
||||
/// Whether this probe is required.
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
/// Extra environment variables for the probe.
|
||||
#[serde(default)]
|
||||
pub env: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl From<PluginToolRequirementDto> for application::PluginToolRequirement {
|
||||
fn from(value: PluginToolRequirementDto) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
executable: value.executable,
|
||||
version_args: value.version_args,
|
||||
required: value.required,
|
||||
env: value.env,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin environment prerequisite DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginEnvRequirementDto {
|
||||
/// Environment variable name.
|
||||
pub name: String,
|
||||
/// Whether this variable is required.
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
/// Optional exact expected value.
|
||||
#[serde(default)]
|
||||
pub equals: Option<String>,
|
||||
}
|
||||
|
||||
impl From<PluginEnvRequirementDto> for application::PluginEnvRequirement {
|
||||
fn from(value: PluginEnvRequirementDto) -> Self {
|
||||
Self {
|
||||
name: value.name,
|
||||
required: value.required,
|
||||
equals: value.equals,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin workspace file prerequisite DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginFileRequirementDto {
|
||||
/// Relative path under project root.
|
||||
pub path: String,
|
||||
/// Whether this path is required.
|
||||
#[serde(default)]
|
||||
pub required: bool,
|
||||
/// Expected kind: `file`, `directory`, or `any`.
|
||||
#[serde(default)]
|
||||
pub kind: Option<String>,
|
||||
}
|
||||
|
||||
impl From<PluginFileRequirementDto> for application::PluginFileRequirement {
|
||||
fn from(value: PluginFileRequirementDto) -> Self {
|
||||
Self {
|
||||
path: value.path,
|
||||
required: value.required,
|
||||
kind: value.kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin external-toolchain diagnostic output DTO.
|
||||
pub type PluginToolchainDiagnosticDto = application::PluginToolchainDiagnostic;
|
||||
|
||||
/// Public plugin event subscription request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginEventSubscribeDto {
|
||||
/// Project id to observe.
|
||||
pub project_id: String,
|
||||
/// Public event types to retain. Empty means all supported types.
|
||||
#[serde(default)]
|
||||
pub event_types: Vec<String>,
|
||||
/// Per-subscription retained event capacity.
|
||||
#[serde(default)]
|
||||
pub capacity: Option<usize>,
|
||||
}
|
||||
|
||||
impl From<PluginEventSubscribeDto> for application::PluginEventSubscribeInput {
|
||||
fn from(value: PluginEventSubscribeDto) -> Self {
|
||||
Self {
|
||||
project_id: value.project_id,
|
||||
event_types: value.event_types,
|
||||
capacity: value.capacity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin event poll request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginEventPollDto {
|
||||
/// Subscription id returned by subscribe.
|
||||
pub subscription_id: String,
|
||||
/// Maximum number of events to drain.
|
||||
#[serde(default)]
|
||||
pub max_events: Option<usize>,
|
||||
}
|
||||
|
||||
impl From<PluginEventPollDto> for application::PluginEventPollInput {
|
||||
fn from(value: PluginEventPollDto) -> Self {
|
||||
Self {
|
||||
subscription_id: value.subscription_id,
|
||||
max_events: value.max_events,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin event unsubscribe request DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginEventUnsubscribeDto {
|
||||
/// Subscription id returned by subscribe.
|
||||
pub subscription_id: String,
|
||||
}
|
||||
|
||||
impl From<PluginEventUnsubscribeDto> for application::PluginEventUnsubscribeInput {
|
||||
fn from(value: PluginEventUnsubscribeDto) -> Self {
|
||||
Self {
|
||||
subscription_id: value.subscription_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public plugin event subscription output DTO.
|
||||
pub type PluginEventSubscriptionDto = application::PluginEventSubscription;
|
||||
/// Public plugin event poll output DTO.
|
||||
pub type PluginEventBatchDto = application::PluginEventBatch;
|
||||
|
||||
/// Plugin workspace text file DTO.
|
||||
pub type PluginWorkspaceTextFileDto = application::PluginWorkspaceTextFile;
|
||||
/// Plugin workspace binary file DTO.
|
||||
pub type PluginWorkspaceBinaryFileDto = application::PluginWorkspaceBinaryFile;
|
||||
/// Plugin workspace directory listing DTO.
|
||||
pub type PluginWorkspaceDirectoryListingDto = application::PluginWorkspaceDirectoryListing;
|
||||
/// Plugin workspace stat DTO.
|
||||
pub type PluginWorkspaceStatDto = application::PluginWorkspaceStat;
|
||||
/// Plugin structured config document DTO.
|
||||
pub type PluginConfigDocumentDto = application::PluginConfigDocument;
|
||||
/// Plugin structured config document write result DTO.
|
||||
pub type PluginConfigDocumentWriteResultDto = application::PluginConfigDocumentWriteResult;
|
||||
/// Plugin project structure result DTO.
|
||||
pub type PluginProjectStructureDto = application::ProjectStructureQuery;
|
||||
|
||||
/// Request DTO for the `health` command.
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -4352,6 +4755,356 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_workspace_requests_use_stable_camel_case_contract() {
|
||||
let path = PluginWorkspacePathDto {
|
||||
project_id: Uuid::from_u128(124).to_string(),
|
||||
path: "src/main.rs".to_owned(),
|
||||
};
|
||||
let text = PluginWorkspaceWriteTextDto {
|
||||
project_id: path.project_id.clone(),
|
||||
path: path.path.clone(),
|
||||
content: "fn main() {}\n".to_owned(),
|
||||
};
|
||||
let binary = PluginWorkspaceWriteBinaryDto {
|
||||
project_id: path.project_id.clone(),
|
||||
path: "assets/icon.bin".to_owned(),
|
||||
bytes: vec![1, 2, 3],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(&path).unwrap(),
|
||||
json!({
|
||||
"projectId": path.project_id,
|
||||
"path": "src/main.rs"
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&text).unwrap(),
|
||||
json!({
|
||||
"projectId": text.project_id,
|
||||
"path": "src/main.rs",
|
||||
"content": "fn main() {}\n"
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&binary).unwrap(),
|
||||
json!({
|
||||
"projectId": binary.project_id,
|
||||
"path": "assets/icon.bin",
|
||||
"bytes": [1, 2, 3]
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_project_structure_query_maps_to_application_input() {
|
||||
let dto = PluginProjectStructureQueryDto {
|
||||
project_id: Uuid::from_u128(129).to_string(),
|
||||
path: Some("crates".to_owned()),
|
||||
max_depth: Some(4),
|
||||
max_entries: Some(250),
|
||||
};
|
||||
|
||||
let json = serde_json::to_value(&dto).unwrap();
|
||||
assert_eq!(
|
||||
json,
|
||||
json!({
|
||||
"projectId": dto.project_id,
|
||||
"path": "crates",
|
||||
"maxDepth": 4,
|
||||
"maxEntries": 250
|
||||
})
|
||||
);
|
||||
|
||||
let input: application::QueryProjectStructureInput = dto.into();
|
||||
assert_eq!(input.path.as_deref(), Some("crates"));
|
||||
assert_eq!(input.max_depth, Some(4));
|
||||
assert_eq!(input.max_entries, Some(250));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_config_document_requests_use_stable_camel_case_contract() {
|
||||
let project_id = Uuid::from_u128(130).to_string();
|
||||
let read = PluginConfigDocumentReadDto {
|
||||
project_id: project_id.clone(),
|
||||
path: "config/settings.json".to_owned(),
|
||||
format: Some("json".to_owned()),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&read).unwrap(),
|
||||
json!({
|
||||
"projectId": project_id,
|
||||
"path": "config/settings.json",
|
||||
"format": "json"
|
||||
})
|
||||
);
|
||||
let input: application::PluginConfigDocumentReadInput = read.into();
|
||||
assert_eq!(input.path, "config/settings.json");
|
||||
assert_eq!(input.format.as_deref(), Some("json"));
|
||||
|
||||
let update = PluginConfigDocumentUpdateDto {
|
||||
project_id: Uuid::from_u128(130).to_string(),
|
||||
path: "config/settings.json".to_owned(),
|
||||
format: Some("json".to_owned()),
|
||||
mode: Some("mergePatch".to_owned()),
|
||||
value: json!({"enabled": true, "removeMe": null}),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&update).unwrap(),
|
||||
json!({
|
||||
"projectId": Uuid::from_u128(130).to_string(),
|
||||
"path": "config/settings.json",
|
||||
"format": "json",
|
||||
"mode": "mergePatch",
|
||||
"value": {
|
||||
"enabled": true,
|
||||
"removeMe": null
|
||||
}
|
||||
})
|
||||
);
|
||||
let input: application::PluginConfigDocumentUpdateInput = update.into();
|
||||
assert_eq!(input.mode.as_deref(), Some("mergePatch"));
|
||||
assert_eq!(input.value["enabled"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_workspace_outputs_use_stable_camel_case_contract() {
|
||||
let listing = PluginWorkspaceDirectoryListingDto {
|
||||
path: "src".to_owned(),
|
||||
entries: vec![application::PluginWorkspaceDirEntry {
|
||||
name: "main.rs".to_owned(),
|
||||
path: "src/main.rs".to_owned(),
|
||||
is_dir: false,
|
||||
}],
|
||||
};
|
||||
let stat = PluginWorkspaceStatDto {
|
||||
path: "src/main.rs".to_owned(),
|
||||
exists: true,
|
||||
is_file: true,
|
||||
is_dir: false,
|
||||
len: Some(13),
|
||||
};
|
||||
let structure = PluginProjectStructureDto {
|
||||
project_id: Uuid::from_u128(129).to_string(),
|
||||
root_path: String::new(),
|
||||
entries: vec![application::ProjectStructureEntry {
|
||||
path: "Cargo.toml".to_owned(),
|
||||
name: "Cargo.toml".to_owned(),
|
||||
kind: "file".to_owned(),
|
||||
}],
|
||||
conventions: vec![application::ProjectConvention {
|
||||
id: "rust-cargo".to_owned(),
|
||||
marker_path: "Cargo.toml".to_owned(),
|
||||
}],
|
||||
modules: vec![application::ProjectModule {
|
||||
path: String::new(),
|
||||
marker_path: "Cargo.toml".to_owned(),
|
||||
convention_id: "rust-cargo".to_owned(),
|
||||
}],
|
||||
truncated: false,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(&listing).unwrap(),
|
||||
json!({
|
||||
"path": "src",
|
||||
"entries": [{
|
||||
"name": "main.rs",
|
||||
"path": "src/main.rs",
|
||||
"isDir": false
|
||||
}]
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&stat).unwrap(),
|
||||
json!({
|
||||
"path": "src/main.rs",
|
||||
"exists": true,
|
||||
"isFile": true,
|
||||
"isDir": false,
|
||||
"len": 13
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&structure).unwrap(),
|
||||
json!({
|
||||
"projectId": structure.project_id,
|
||||
"rootPath": "",
|
||||
"entries": [{
|
||||
"path": "Cargo.toml",
|
||||
"name": "Cargo.toml",
|
||||
"kind": "file"
|
||||
}],
|
||||
"conventions": [{
|
||||
"id": "rust-cargo",
|
||||
"markerPath": "Cargo.toml"
|
||||
}],
|
||||
"modules": [{
|
||||
"path": "",
|
||||
"markerPath": "Cargo.toml",
|
||||
"conventionId": "rust-cargo"
|
||||
}],
|
||||
"truncated": false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_command_task_requests_use_stable_camel_case_contract() {
|
||||
let project_id = Uuid::from_u128(125).to_string();
|
||||
let owner_agent_id = Uuid::from_u128(126).to_string();
|
||||
let run = PluginRunCommandDto {
|
||||
project_id: project_id.clone(),
|
||||
owner_agent_id: owner_agent_id.clone(),
|
||||
label: "cargo test".to_owned(),
|
||||
command: "cargo".to_owned(),
|
||||
args: vec!["test".to_owned(), "-p".to_owned(), "application".to_owned()],
|
||||
cwd: Some("crates/application".to_owned()),
|
||||
env: vec![("RUST_LOG".to_owned(), "debug".to_owned())],
|
||||
record_only: true,
|
||||
deadline_ms: Some(1_800_000_000_000),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(&run).unwrap(),
|
||||
json!({
|
||||
"projectId": project_id,
|
||||
"ownerAgentId": owner_agent_id,
|
||||
"label": "cargo test",
|
||||
"command": "cargo",
|
||||
"args": ["test", "-p", "application"],
|
||||
"cwd": "crates/application",
|
||||
"env": [["RUST_LOG", "debug"]],
|
||||
"recordOnly": true,
|
||||
"deadlineMs": 1_800_000_000_000u64
|
||||
})
|
||||
);
|
||||
|
||||
let input: application::PluginRunCommandInput = run.into();
|
||||
assert_eq!(input.cwd.as_deref(), Some("crates/application"));
|
||||
assert_eq!(input.env, vec![("RUST_LOG".to_owned(), "debug".to_owned())]);
|
||||
assert!(input.record_only);
|
||||
|
||||
let status = PluginTaskStatusDto {
|
||||
task_id: Uuid::from_u128(127).to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&status).unwrap(),
|
||||
json!({ "taskId": status.task_id })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_toolchain_diagnostic_request_maps_to_application_input() {
|
||||
let project_id = Uuid::from_u128(126).to_string();
|
||||
let request = PluginToolchainDiagnosticRequestDto {
|
||||
project_id: project_id.clone(),
|
||||
cwd: Some("crates/backend".to_owned()),
|
||||
tools: vec![PluginToolRequirementDto {
|
||||
id: "rust".to_owned(),
|
||||
executable: "cargo".to_owned(),
|
||||
version_args: vec!["--version".to_owned()],
|
||||
required: true,
|
||||
env: vec![("CARGO_TERM_COLOR".to_owned(), "never".to_owned())],
|
||||
}],
|
||||
env: vec![PluginEnvRequirementDto {
|
||||
name: "RUSTUP_HOME".to_owned(),
|
||||
required: false,
|
||||
equals: None,
|
||||
}],
|
||||
files: vec![PluginFileRequirementDto {
|
||||
path: "Cargo.toml".to_owned(),
|
||||
required: true,
|
||||
kind: Some("file".to_owned()),
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(&request).unwrap(),
|
||||
json!({
|
||||
"projectId": project_id,
|
||||
"cwd": "crates/backend",
|
||||
"tools": [{
|
||||
"id": "rust",
|
||||
"executable": "cargo",
|
||||
"versionArgs": ["--version"],
|
||||
"required": true,
|
||||
"env": [["CARGO_TERM_COLOR", "never"]]
|
||||
}],
|
||||
"env": [{
|
||||
"name": "RUSTUP_HOME",
|
||||
"required": false,
|
||||
"equals": null
|
||||
}],
|
||||
"files": [{
|
||||
"path": "Cargo.toml",
|
||||
"required": true,
|
||||
"kind": "file"
|
||||
}]
|
||||
})
|
||||
);
|
||||
|
||||
let input: application::PluginToolchainDiagnosticInput = request.into();
|
||||
assert_eq!(input.cwd.as_deref(), Some("crates/backend"));
|
||||
assert_eq!(input.tools[0].id, "rust");
|
||||
assert_eq!(input.tools[0].env[0].0, "CARGO_TERM_COLOR");
|
||||
assert_eq!(input.env[0].name, "RUSTUP_HOME");
|
||||
assert_eq!(input.files[0].kind.as_deref(), Some("file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dto_plugins_event_subscription_requests_use_stable_camel_case_contract() {
|
||||
let project_id = Uuid::from_u128(127).to_string();
|
||||
let subscribe = PluginEventSubscribeDto {
|
||||
project_id: project_id.clone(),
|
||||
event_types: vec![
|
||||
"workspaceFileChanged".to_owned(),
|
||||
"backgroundTaskChanged".to_owned(),
|
||||
],
|
||||
capacity: Some(250),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&subscribe).unwrap(),
|
||||
json!({
|
||||
"projectId": project_id,
|
||||
"eventTypes": ["workspaceFileChanged", "backgroundTaskChanged"],
|
||||
"capacity": 250
|
||||
})
|
||||
);
|
||||
let input: application::PluginEventSubscribeInput = subscribe.into();
|
||||
assert_eq!(
|
||||
input.event_types,
|
||||
vec![
|
||||
"workspaceFileChanged".to_owned(),
|
||||
"backgroundTaskChanged".to_owned()
|
||||
]
|
||||
);
|
||||
assert_eq!(input.capacity, Some(250));
|
||||
|
||||
let poll = PluginEventPollDto {
|
||||
subscription_id: Uuid::from_u128(128).to_string(),
|
||||
max_events: Some(50),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&poll).unwrap(),
|
||||
json!({
|
||||
"subscriptionId": poll.subscription_id,
|
||||
"maxEvents": 50
|
||||
})
|
||||
);
|
||||
let input: application::PluginEventPollInput = poll.into();
|
||||
assert_eq!(input.max_events, Some(50));
|
||||
|
||||
let unsubscribe = PluginEventUnsubscribeDto {
|
||||
subscription_id: Uuid::from_u128(129).to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::to_value(&unsubscribe).unwrap(),
|
||||
json!({ "subscriptionId": unsubscribe.subscription_id })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn background_task_dto_exposes_rendezvous_context_only_for_headless_rendezvous() {
|
||||
let project_id = ProjectId::from_uuid(Uuid::from_u128(1));
|
||||
|
||||
@ -401,6 +401,16 @@ pub enum DomainEventDto {
|
||||
/// Project id.
|
||||
project_id: String,
|
||||
},
|
||||
/// A workspace file changed through the public plugin workspace API.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
PluginWorkspaceFileChanged {
|
||||
/// Project id.
|
||||
project_id: String,
|
||||
/// Relative workspace path.
|
||||
path: String,
|
||||
/// Public operation label.
|
||||
operation: String,
|
||||
},
|
||||
/// An issue-backed public ticket was created.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IssueCreated {
|
||||
@ -1058,6 +1068,15 @@ impl From<&DomainEvent> for DomainEventDto {
|
||||
DomainEvent::GitStateChanged { project_id } => Self::GitStateChanged {
|
||||
project_id: project_id.to_string(),
|
||||
},
|
||||
DomainEvent::PluginWorkspaceFileChanged {
|
||||
project_id,
|
||||
path,
|
||||
operation,
|
||||
} => Self::PluginWorkspaceFileChanged {
|
||||
project_id: project_id.to_string(),
|
||||
path: path.clone(),
|
||||
operation: operation.clone(),
|
||||
},
|
||||
DomainEvent::IssueCreated {
|
||||
issue_id,
|
||||
issue_ref,
|
||||
|
||||
@ -35,37 +35,39 @@ use application::{
|
||||
MarkIssueAttachmentSummarized, McpRuntime, McpToolPermissionCatalogue, MoveTabToNewWindow,
|
||||
MutateLayout, OnnxModelView, OpenProject, OpenTerminal, OpenTicketAssistant,
|
||||
OrchestratorService, PairAttemptLimiter, PairDevice, PermissionProjectorRegistry,
|
||||
ProposeContext, ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue,
|
||||
ReadIssueAttachment, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory, ReadMemoryIndex,
|
||||
ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
|
||||
ReconcileLiveState, ReconcileLiveStateInput, ReconcilePluginMcpServers, RecordTurn,
|
||||
RecordTurnProvider, ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint,
|
||||
ReorderSprints, ResizeTerminal, ResolveAgentCapabilities, ResolveAgentPermissions,
|
||||
ResolveAgentSystemPermissions, ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask,
|
||||
ReviewPluginPackage, RevokeAllDevices, RevokeDevice, RotateConversationLog,
|
||||
SaveEmbedderProfile, SaveModelServer, SaveOpenCodeProviderProfile, SaveProfile,
|
||||
SessionLimitService, SetActiveLayout, SetPluginEnabled, SnapshotOpenWindows,
|
||||
SnapshotRunningAgents, SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode,
|
||||
StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice,
|
||||
UnassignSkillFromAgent, UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues,
|
||||
UpdateAgentContext, UpdateAgentMcpToolPermissions, UpdateAgentPermissions,
|
||||
UpdateAgentSystemPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory,
|
||||
UpdateProjectContext, UpdateProjectMcpToolPermissions, UpdateProjectPermissions,
|
||||
UpdateProjectSystemPermissions, UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory,
|
||||
WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
||||
PluginCommandTasks, PluginConfigDocuments, PluginEventSubscriptions,
|
||||
PluginToolchainDiagnostics, PluginWorkspaceAccess, ProposeContext, QueryProjectStructure,
|
||||
ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue, ReadIssueAttachment,
|
||||
ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory, ReadMemoryIndex, ReadProjectContext,
|
||||
ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts, ReconcileLiveState,
|
||||
ReconcileLiveStateInput, ReconcilePluginMcpServers, RecordTurn, RecordTurnProvider,
|
||||
ReferenceProfiles, RenameDevice, RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal,
|
||||
ResolveAgentCapabilities, ResolveAgentPermissions, ResolveAgentSystemPermissions,
|
||||
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, ReviewPluginPackage,
|
||||
RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveEmbedderProfile, SaveModelServer,
|
||||
SaveOpenCodeProviderProfile, SaveProfile, SessionLimitService, SetActiveLayout,
|
||||
SetPluginEnabled, SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand,
|
||||
StopLiveAgent, StructuredRoutingMode, StructuredSessions, SuggestedThisSession,
|
||||
SyncAgentWithTemplate, TerminalSessions, TouchDevice, UnassignSkillFromAgent,
|
||||
UnassignTicketFromSprint, UninstallPlugin, UnlinkIssues, UpdateAgentContext,
|
||||
UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateAgentSystemPermissions,
|
||||
UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory, UpdateProjectContext,
|
||||
UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateProjectSystemPermissions,
|
||||
UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal,
|
||||
AGENT_MEMORY_RECALL_BUDGET,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, AgentSession, AgentSessionFactory, AgentToolPolicyStore,
|
||||
AgentWakePort, AssistantContextProvider, BackgroundTaskPortError, BackgroundTaskRunner,
|
||||
BackgroundTaskStore, Clock, DeviceSessionStore, Embedder, EmbedderEnvInspector,
|
||||
EmbedderProfileStore, EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator,
|
||||
IssueNumberAllocator, IssueStore, McpToolPermissionStore, MemoryRecall, MemoryStore,
|
||||
ModelArtifactDownloader, PermissionStore, PluginManifestValidator, PluginMcpSupervisor,
|
||||
PluginPackageStore, PluginRegistryStore, ProcessSpawner, ProfileStore, ProjectStore, PtyHandle,
|
||||
PtyPort, RuntimePermissionProbe, ScheduledTask, Scheduler, SecretStore, SkillStore,
|
||||
SprintStore, StructuredSessionEnvironmentPreparer, SystemPermissionStore, TemplateStore,
|
||||
ToolInvoker, WakeError, WakeReason, WindowStateStore,
|
||||
EmbedderProfileStore, EmbedderPromptStore, EnvironmentReader, EventBus, FileSystem, GitPort,
|
||||
IdGenerator, IssueNumberAllocator, IssueStore, McpToolPermissionStore, MemoryRecall,
|
||||
MemoryStore, ModelArtifactDownloader, PermissionStore, PluginManifestValidator,
|
||||
PluginMcpSupervisor, PluginPackageStore, PluginRegistryStore, ProcessSpawner, ProfileStore,
|
||||
ProjectStore, PtyHandle, PtyPort, RuntimePermissionProbe, ScheduledTask, Scheduler,
|
||||
SecretStore, SkillStore, SprintStore, StructuredSessionEnvironmentPreparer,
|
||||
SystemPermissionStore, TemplateStore, ToolInvoker, WakeError, WakeReason, WindowStateStore,
|
||||
};
|
||||
use domain::profile::{
|
||||
AgentProfile, ContextInjection, McpConfigStrategy, McpTransport, StructuredAdapter,
|
||||
@ -93,13 +95,14 @@ use infrastructure::{
|
||||
FsSprintStore, FsSystemPermissionStore, FsTemplateStore, FsWindowStateStore, Git2Repository,
|
||||
HeuristicHandoffSummarizer, HfModelArtifactDownloader, HttpOpenAiCompatibleProbe,
|
||||
HttpProviderModelCatalogue, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
|
||||
InMemoryPairAttemptLimiter, LlamaCppRuntime, LocalFileSystem, LocalManagedProcess,
|
||||
LocalProcessSpawner, McpServer, MediatedInbox, NaiveMemoryRecall, OrchestratorWatchHandle,
|
||||
PortablePtyAdapter, ProcessCliVersionReader, ReadOnlyRuntimePermissionProbe, RwFileGuard,
|
||||
StructuredSessionFactory, SystemClock, SystemMillisClock, TemplateToolProvider,
|
||||
TicketAssistantEnvironmentPreparer, TicketToolProvider, TokioBroadcastEventBus, TokioScheduler,
|
||||
ToolPolicyRegistry, UuidGenerator, VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL,
|
||||
ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
|
||||
InMemoryPairAttemptLimiter, LlamaCppRuntime, LocalEnvironmentReader, LocalFileSystem,
|
||||
LocalManagedProcess, LocalProcessSpawner, McpServer, MediatedInbox, NaiveMemoryRecall,
|
||||
OrchestratorWatchHandle, PortablePtyAdapter, ProcessCliVersionReader,
|
||||
ReadOnlyRuntimePermissionProbe, RwFileGuard, StructuredSessionFactory, SystemClock,
|
||||
SystemMillisClock, TemplateToolProvider, TicketAssistantEnvironmentPreparer,
|
||||
TicketToolProvider, TokioBroadcastEventBus, TokioScheduler, ToolPolicyRegistry, UuidGenerator,
|
||||
VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS,
|
||||
VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
|
||||
};
|
||||
|
||||
pub mod dto;
|
||||
@ -1135,6 +1138,18 @@ pub struct BackendCore {
|
||||
pub list_plugin_runtime_contributions: Arc<ListPluginRuntimeContributions>,
|
||||
/// Reconcile external MCP plugin servers.
|
||||
pub reconcile_plugin_mcp_servers: Arc<ReconcilePluginMcpServers>,
|
||||
/// Public plugin workspace/file access facade.
|
||||
pub plugin_workspace_access: Arc<PluginWorkspaceAccess>,
|
||||
/// Public plugin structured config document facade.
|
||||
pub plugin_config_documents: Arc<PluginConfigDocuments>,
|
||||
/// Public plugin project-structure query use case.
|
||||
pub query_project_structure: Arc<QueryProjectStructure>,
|
||||
/// Public plugin command/task facade.
|
||||
pub plugin_command_tasks: Arc<PluginCommandTasks>,
|
||||
/// Public plugin external-toolchain diagnostic facade.
|
||||
pub plugin_toolchain_diagnostics: Arc<PluginToolchainDiagnostics>,
|
||||
/// Public plugin event subscription facade.
|
||||
pub plugin_event_subscriptions: Arc<PluginEventSubscriptions>,
|
||||
/// Package store exposed for the Tauri asset protocol adapter.
|
||||
pub plugin_package_store: Arc<FsPluginPackageStore>,
|
||||
/// Registry store exposed for the Tauri asset protocol adapter.
|
||||
@ -1553,6 +1568,8 @@ impl BackendCore {
|
||||
// registry.
|
||||
let spawner = Arc::new(LocalProcessSpawner::new());
|
||||
let spawner_port = Arc::clone(&spawner) as Arc<dyn ProcessSpawner>;
|
||||
let environment_reader = Arc::new(LocalEnvironmentReader::new());
|
||||
let environment_reader_port = Arc::clone(&environment_reader) as Arc<dyn EnvironmentReader>;
|
||||
let runtime = Arc::new(CliAgentRuntime::new(Arc::clone(&spawner_port)));
|
||||
let runtime_port = Arc::clone(&runtime) as Arc<dyn AgentRuntime>;
|
||||
|
||||
@ -2358,12 +2375,15 @@ impl BackendCore {
|
||||
let _ = drain.await;
|
||||
});
|
||||
}
|
||||
let spawn_background_command = Arc::new(SpawnBackgroundCommand::new(
|
||||
Arc::clone(&background_tasks_port),
|
||||
Arc::clone(&background_runner_port),
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
));
|
||||
let spawn_background_command = Arc::new(
|
||||
SpawnBackgroundCommand::new(
|
||||
Arc::clone(&background_tasks_port),
|
||||
Arc::clone(&background_runner_port),
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
)
|
||||
.with_events(Arc::clone(&events_port)),
|
||||
);
|
||||
let cancel_background_task = Arc::new(CancelBackgroundTask::new(
|
||||
Arc::clone(&background_tasks_port),
|
||||
Arc::clone(&background_runner_port),
|
||||
@ -2420,6 +2440,47 @@ impl BackendCore {
|
||||
Arc::clone(&plugin_manifest_validator),
|
||||
Arc::clone(&plugin_mcp_supervisor_port),
|
||||
));
|
||||
let plugin_workspace_access = Arc::new(
|
||||
PluginWorkspaceAccess::new(Arc::clone(&store_port), Arc::clone(&fs_port))
|
||||
.with_events(Arc::clone(&events_port)),
|
||||
);
|
||||
let plugin_config_documents = Arc::new(
|
||||
PluginConfigDocuments::new(Arc::clone(&store_port), Arc::clone(&fs_port))
|
||||
.with_events(Arc::clone(&events_port)),
|
||||
);
|
||||
let query_project_structure = Arc::new(QueryProjectStructure::new(
|
||||
Arc::clone(&store_port),
|
||||
Arc::clone(&fs_port),
|
||||
));
|
||||
let plugin_command_tasks = Arc::new(PluginCommandTasks::new(
|
||||
Arc::clone(&store_port),
|
||||
Arc::clone(&background_tasks_port),
|
||||
Arc::clone(&spawn_background_command),
|
||||
));
|
||||
let plugin_toolchain_diagnostics = Arc::new(PluginToolchainDiagnostics::new(
|
||||
Arc::clone(&store_port),
|
||||
Arc::clone(&fs_port),
|
||||
Arc::clone(&spawner_port),
|
||||
Arc::clone(&environment_reader_port),
|
||||
));
|
||||
let plugin_event_subscriptions = Arc::new(PluginEventSubscriptions::new(
|
||||
Arc::clone(&store_port),
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
));
|
||||
{
|
||||
let mut rx = event_bus.raw_receiver();
|
||||
let subscriptions = Arc::clone(&plugin_event_subscriptions);
|
||||
spawn_detached(async move {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(event) => subscriptions.record_domain_event(&event),
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
let background_wake = Arc::new(AgentWakeService::new(
|
||||
Arc::clone(&mediated_inbox) as Arc<dyn AgentInbox>,
|
||||
Arc::clone(&input_mediator),
|
||||
@ -2982,6 +3043,12 @@ impl BackendCore {
|
||||
uninstall_plugin,
|
||||
list_plugin_runtime_contributions,
|
||||
reconcile_plugin_mcp_servers,
|
||||
plugin_workspace_access,
|
||||
plugin_config_documents,
|
||||
query_project_structure,
|
||||
plugin_command_tasks,
|
||||
plugin_toolchain_diagnostics,
|
||||
plugin_event_subscriptions,
|
||||
plugin_package_store: Arc::clone(&plugin_packages),
|
||||
plugin_registry_store: Arc::clone(&plugin_registry_store),
|
||||
plugin_manifest_validator: Arc::clone(&plugin_manifest_validator),
|
||||
|
||||
@ -366,6 +366,15 @@ pub enum DomainEvent {
|
||||
/// The project.
|
||||
project_id: ProjectId,
|
||||
},
|
||||
/// A file under a project workspace changed through a public plugin workspace API.
|
||||
PluginWorkspaceFileChanged {
|
||||
/// The owning project.
|
||||
project_id: ProjectId,
|
||||
/// Normalized path relative to the project root.
|
||||
path: String,
|
||||
/// Public operation label, for example `changed`.
|
||||
operation: String,
|
||||
},
|
||||
/// An orchestrator request (dropped under `.ideai/requests/`) was processed
|
||||
/// by IdeA on behalf of a requester agent (ARCHITECTURE §14.3). Relayed so the
|
||||
/// frontend can surface orchestration activity; the resulting cell/tab opens
|
||||
|
||||
@ -480,6 +480,17 @@ pub struct DirEntry {
|
||||
pub is_dir: bool,
|
||||
}
|
||||
|
||||
/// Basic metadata returned by [`FileSystem::metadata`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FileMetadata {
|
||||
/// Whether the path points to a regular file.
|
||||
pub is_file: bool,
|
||||
/// Whether the path points to a directory.
|
||||
pub is_dir: bool,
|
||||
/// File length in bytes when known.
|
||||
pub len: Option<u64>,
|
||||
}
|
||||
|
||||
/// An owned, boxed stream of PTY output chunks.
|
||||
///
|
||||
/// Concrete adapters decide the underlying transport; the domain only sees a
|
||||
@ -1281,6 +1292,12 @@ pub trait ProcessSpawner: Send + Sync {
|
||||
async fn run(&self, spec: SpawnSpec) -> Result<Output, ProcessError>;
|
||||
}
|
||||
|
||||
/// Reads host environment variables through an injected adapter.
|
||||
pub trait EnvironmentReader: Send + Sync {
|
||||
/// Returns one environment variable value, if present.
|
||||
fn get(&self, name: &str) -> Option<String>;
|
||||
}
|
||||
|
||||
/// Read a local structured CLI version using only the allowed `--version` probe.
|
||||
#[async_trait]
|
||||
pub trait CliVersionReader: Send + Sync {
|
||||
@ -1526,6 +1543,25 @@ pub trait FileSystem: Send + Sync {
|
||||
/// [`FsError`] on failure.
|
||||
async fn exists(&self, path: &RemotePath) -> Result<bool, FsError>;
|
||||
|
||||
/// Returns basic metadata for a path.
|
||||
///
|
||||
/// The default keeps older remote/test adapters source-compatible. Concrete
|
||||
/// adapters that can cheaply stat paths should override it.
|
||||
///
|
||||
/// # Errors
|
||||
/// [`FsError`] on failure.
|
||||
async fn metadata(&self, path: &RemotePath) -> Result<FileMetadata, FsError> {
|
||||
if self.exists(path).await? {
|
||||
Ok(FileMetadata {
|
||||
is_file: false,
|
||||
is_dir: false,
|
||||
len: None,
|
||||
})
|
||||
} else {
|
||||
Err(FsError::NotFound(path.as_str().to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a single file. A **missing** file is treated as success (idempotent
|
||||
/// delete), so this is safe to call best-effort.
|
||||
///
|
||||
|
||||
@ -9,7 +9,7 @@ use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{DirEntry, FileSystem, FsError, RemotePath};
|
||||
use domain::ports::{DirEntry, FileMetadata, FileSystem, FsError, RemotePath};
|
||||
use tokio::fs;
|
||||
|
||||
/// Filesystem adapter backed by the local OS via `tokio::fs`.
|
||||
@ -54,6 +54,17 @@ impl FileSystem for LocalFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
async fn metadata(&self, path: &RemotePath) -> Result<FileMetadata, FsError> {
|
||||
let meta = fs::metadata(path.as_str())
|
||||
.await
|
||||
.map_err(|e| map_io(path, &e))?;
|
||||
Ok(FileMetadata {
|
||||
is_file: meta.is_file(),
|
||||
is_dir: meta.is_dir(),
|
||||
len: Some(meta.len()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn remove_file(&self, path: &RemotePath) -> Result<(), FsError> {
|
||||
match fs::remove_file(path.as_str()).await {
|
||||
Ok(()) => Ok(()),
|
||||
|
||||
@ -89,7 +89,7 @@ pub use orchestrator::{
|
||||
pub use pair_attempt_limiter::InMemoryPairAttemptLimiter;
|
||||
pub use permission::{ClaudePermissionProjector, CodexPermissionProjector};
|
||||
pub use plugin::{ExternalMcpPluginSupervisor, FsPluginPackageStore, FsPluginRegistryStore};
|
||||
pub use process::LocalProcessSpawner;
|
||||
pub use process::{LocalEnvironmentReader, LocalProcessSpawner};
|
||||
pub use pty::PortablePtyAdapter;
|
||||
pub use ratelimit::RateLimitParser;
|
||||
pub use remote::{remote_host, LocalHost};
|
||||
|
||||
@ -9,7 +9,9 @@
|
||||
use async_trait::async_trait;
|
||||
use tokio::process::Command;
|
||||
|
||||
use domain::ports::{ExitStatus, Output, ProcessError, ProcessSpawner, SpawnSpec};
|
||||
use domain::ports::{
|
||||
EnvironmentReader, ExitStatus, Output, ProcessError, ProcessSpawner, SpawnSpec,
|
||||
};
|
||||
|
||||
/// Process spawner backed by the local OS via `tokio::process::Command`.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
@ -23,6 +25,24 @@ impl LocalProcessSpawner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Environment reader backed by the local process environment.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct LocalEnvironmentReader;
|
||||
|
||||
impl LocalEnvironmentReader {
|
||||
/// Creates a new [`LocalEnvironmentReader`].
|
||||
#[must_use]
|
||||
pub const fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvironmentReader for LocalEnvironmentReader {
|
||||
fn get(&self, name: &str) -> Option<String> {
|
||||
std::env::var(name).ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ProcessSpawner for LocalProcessSpawner {
|
||||
async fn run(&self, spec: SpawnSpec) -> Result<Output, ProcessError> {
|
||||
|
||||
Reference in New Issue
Block a user