Skip to main content

pharos_native/runtime/
agent_architect.rs

1use serde_json::Value;
2use std::path::{Component, Path, PathBuf};
3
4use super::agent_bridge::{self, AgentErrorKind, PathClassification};
5pub use pharos_runtime_architect::editor_state::load_architect_editor_source_view_from_root;
6use pharos_runtime_architect::lifecycle_validation::{
7    ArchitectLifecycleTools,
8    apply_and_validate_review_workspace as architect_apply_and_validate_review_workspace,
9    architect_current_dir_lock,
10};
11use pharos_runtime_architect::llm_vendor::ArchitectLlmSecretProvider;
12pub use pharos_runtime_architect::source_bundle::source_export_relative_path_allowed;
13pub use pharos_runtime_architect::source_export::{
14    ArchitectSourceExport, ArchitectSourceExportDownload, ArchitectSourceExportKind,
15    export_architect_source, export_kind_slug, parse_source_export_kind,
16    read_architect_source_export, validate_source_export_id,
17};
18pub use pharos_runtime_architect::task_planning::prepare_architect_task_plan;
19pub use pharos_runtime_architect::task_state::{
20    ArchitectEditorProjectDeleteResult, ArchitectEditorProjectDuplicateResult,
21    ArchitectEditorProjectRenameResult, ArchitectEditorRollbackResult,
22    ArchitectEditorSelectedSource, ArchitectEditorSourceAppImportResult,
23    ArchitectEditorSourceEditApplyResult, ArchitectEditorSourceEditPlan, ArchitectEditorSourceFile,
24    ArchitectEditorSourceView, ArchitectEditorStructuredEditPlan, ArchitectLlmVendorVaultPaths,
25    ArchitectTaskApplyResult, ArchitectTaskDeleteResult, ArchitectWorkerLoopRun,
26    ArchitectWorkerRun, ArchitectWorkerTaskResult,
27};
28
29/// Resolves typed Architect entities through the configured primary database adapter.
30pub fn architect_database_storage(
31    app_root: &Path,
32    artifact_root: &Path,
33    runtime_conf: Option<&Path>,
34    entities: &[&str],
35) -> Result<
36    (
37        pharos_runtime_database::DbBackendConfig,
38        std::sync::Arc<dyn pharos_runtime_database::operations::DatabaseOperations>,
39        pharos_runtime_database::operations::AppSchema,
40    ),
41    AgentErrorKind,
42> {
43    let config =
44        super::database::load_cli_backend_config(app_root, runtime_conf, Some("architect"))
45            .ok_or(AgentErrorKind::InvalidPath)?;
46    let adapter = super::database::operations::resolve(&config)
47        .map_err(|_| AgentErrorKind::PolicyViolation)?;
48    let contract = super::database::selected_logical_contract(artifact_root, "architect")
49        .map_err(|_| AgentErrorKind::PolicyViolation)?
50        .ok_or(AgentErrorKind::PolicyViolation)?;
51    if entities.is_empty() || entities.len() > 256 {
52        return Err(AgentErrorKind::PolicyViolation);
53    }
54    let mut requested = std::collections::BTreeSet::new();
55    for name in entities {
56        if !requested.insert(*name)
57            || !contract
58                .schema
59                .entities
60                .iter()
61                .any(|entity| entity.name == *name)
62        {
63            return Err(AgentErrorKind::PolicyViolation);
64        }
65    }
66    // Keep the complete selected schema so references and constraints retain
67    // the same meaning for workers, HTTP callers and migration readiness.
68    let schema = contract.schema;
69    Ok((config, adapter, schema))
70}
71
72/// Loads the worker's declared Architect entities through one configured adapter.
73pub fn architect_worker_store(
74    app_root: &Path,
75    artifact_root: &Path,
76    runtime_conf: Option<&Path>,
77) -> Result<pharos_runtime_architect::database::ArchitectStore, AgentErrorKind> {
78    let (config, adapter, schema) = architect_database_storage(
79        app_root,
80        artifact_root,
81        runtime_conf,
82        &[
83            "architect_task",
84            "architect_task_event",
85            "architect_attachment_extract",
86            "architect_attachment",
87            "architect_transcript",
88            "architect_llm_vendor",
89            "architect_editor_project",
90            "architect_editor_version",
91            "architect_editor_source_file",
92            "architect_editor_change",
93            "architect_source_export",
94            "architect_vault_audit",
95            "architect_user_activity",
96            "architect_workspace",
97            "architect_privacy_setting",
98            "architect_target_profile",
99            "user_account",
100            "store_membership",
101            "auth_session",
102            "rate_limit_event",
103        ],
104    )?;
105    pharos_runtime_architect::database::ArchitectStore::new(config, adapter, schema)
106        .map_err(|_| AgentErrorKind::PolicyViolation)
107}
108
109/// Loads a validated Architect editor version through the logical database store.
110pub fn load_architect_editor_source_view_in_store(
111    store: &pharos_runtime_architect::database::ArchitectStore,
112    version_id: i64,
113    selected_path: Option<&str>,
114) -> Result<ArchitectEditorSourceView, AgentErrorKind> {
115    pharos_runtime_architect::editor_state::load_editor_source_view_in_store(
116        store,
117        version_id,
118        selected_path,
119    )
120}
121
122/// Plans one source-file edit against the current validated editor version.
123pub fn prepare_architect_editor_source_edit_in_store(
124    store: &pharos_runtime_architect::database::ArchitectStore,
125    version_id: i64,
126    source_path: &str,
127    proposed_content: &str,
128    actor_id: i64,
129) -> Result<ArchitectEditorSourceEditPlan, AgentErrorKind> {
130    pharos_runtime_architect::editor_state::prepare_editor_changes_in_store(
131        store,
132        version_id,
133        &[(source_path.to_owned(), proposed_content.to_owned())],
134        actor_id,
135        "source_edit",
136    )
137}
138
139/// Plans a declarative entity, field, or layout edit through the same source ledger.
140pub fn prepare_architect_editor_structured_edit_in_store(
141    store: &pharos_runtime_architect::database::ArchitectStore,
142    version_id: i64,
143    edit_kind: &str,
144    params: &Value,
145    actor_id: i64,
146) -> Result<ArchitectEditorStructuredEditPlan, AgentErrorKind> {
147    if version_id <= 0 || actor_id <= 0 {
148        return Err(AgentErrorKind::InvalidIdentifier);
149    }
150    let edit_kind =
151        pharos_runtime_architect::editor_state::normalize_structured_edit_kind(edit_kind)?;
152    let source_view = load_architect_editor_source_view_in_store(store, version_id, None)?;
153    let changes = pharos_runtime_architect::editor_state::prepare_structured_editor_changes(
154        &source_view,
155        edit_kind,
156        params,
157    )?;
158    if changes.is_empty() {
159        return Err(AgentErrorKind::StalePlan);
160    }
161    let owned = changes
162        .into_iter()
163        .map(|change| (change.source_path.to_owned(), change.content))
164        .collect::<Vec<_>>();
165    let plan = pharos_runtime_architect::editor_state::prepare_editor_changes_in_store(
166        store,
167        version_id,
168        &owned,
169        actor_id,
170        match edit_kind {
171            "layout_pattern" => "page_edit",
172            "entity_add" | "entity_rename" | "entity_remove" => "entity_edit",
173            _ => "field_edit",
174        },
175    )?;
176    Ok(ArchitectEditorStructuredEditPlan {
177        schema: "pharos.architect.editor_structured_edit_plan.v1".to_owned(),
178        edit_kind: edit_kind.to_owned(),
179        project_id: source_view.project_id,
180        version_id,
181        plans: vec![plan],
182    })
183}
184
185/// Renames an Architect project through the app's configured primary backend.
186pub fn rename_architect_editor_project_for_app(
187    app: &crate::config::AppRuntime,
188    project_id: i64,
189    title: &str,
190) -> Result<ArchitectEditorProjectRenameResult, AgentErrorKind> {
191    if app.app_id.as_deref() != Some("architect") {
192        return Err(AgentErrorKind::PolicyViolation);
193    }
194    let app_root = app
195        .app_root
196        .as_deref()
197        .map(Path::new)
198        .ok_or(AgentErrorKind::InvalidPath)?;
199    let artifact_root = app
200        .artifact_dir
201        .as_deref()
202        .map(Path::new)
203        .unwrap_or(app_root);
204    let (config, adapter, schema) = architect_database_storage(
205        app_root,
206        artifact_root,
207        app.runtime_conf_path.as_deref().map(Path::new),
208        &["architect_editor_project"],
209    )?;
210    pharos_runtime_architect::project_state::rename_architect_editor_project(
211        &config,
212        adapter.as_ref(),
213        &schema,
214        project_id,
215        title,
216    )
217}
218
219struct RootArchitectLifecycleTools<'a> {
220    connection_task: Option<(&'a pharos_runtime_architect::database::ArchitectStore, i64)>,
221}
222
223impl ArchitectLifecycleTools for RootArchitectLifecycleTools<'_> {
224    /// Rechecks a subscription task without coupling ordinary edits to a provider connection.
225    fn before_source_promotion(&mut self) -> Result<(), AgentErrorKind> {
226        if let Some((store, task_id)) = self.connection_task {
227            let task = pharos_runtime_architect::task_repository::load_task(store, task_id)?;
228            if task.llm_vendor_id == "chatgpt" {
229                pharos_runtime_architect::chatgpt::repository::connection_for_task(store, task_id)
230                    .map_err(|_| AgentErrorKind::CredentialUnavailable)?;
231            }
232        }
233        Ok(())
234    }
235    fn apply_change(&mut self, app_id: &str, plan_digest: &str) -> Result<Value, AgentErrorKind> {
236        agent_bridge::apply_app_change(app_id, plan_digest)
237    }
238
239    fn run_app_tool(
240        &mut self,
241        tool: &str,
242        app_id: &str,
243        app_root: Option<&str>,
244        build_dir: Option<&str>,
245        config_path: Option<&str>,
246    ) -> Result<Value, AgentErrorKind> {
247        let result =
248            agent_bridge::run_lifecycle_app_tool(tool, app_id, app_root, build_dir, config_path);
249        if let Err(error) = &result {
250            eprintln!("architect lifecycle {tool} failed: {error:?}");
251        }
252        result
253    }
254
255    fn seed_app(
256        &mut self,
257        app_id: &str,
258        app_root: Option<&str>,
259        config_path: Option<&str>,
260        backend: &str,
261    ) -> Result<Value, AgentErrorKind> {
262        let result = agent_bridge::seed_app_tool(app_id, app_root, config_path, backend);
263        if let Err(error) = &result {
264            eprintln!("architect lifecycle seed failed: {error:?}");
265        }
266        result
267    }
268}
269
270struct RootArchitectLlmSecretProvider;
271
272impl ArchitectLlmSecretProvider for RootArchitectLlmSecretProvider {
273    fn get_llm_vendor_secret(
274        &self,
275        vault_path: &Path,
276        root_key_file: &Path,
277        llm_vendor_id: &str,
278    ) -> Result<Vec<u8>, AgentErrorKind> {
279        super::agent_vault::get_architect_runtime_llm_vendor_secret(
280            vault_path,
281            root_key_file,
282            llm_vendor_id,
283        )
284    }
285}
286
287/// Runs one bounded Architect worker iteration against the configured database.
288pub fn run_architect_worker_once(
289    store: &pharos_runtime_architect::database::ArchitectStore,
290    output_root: &Path,
291) -> Result<ArchitectWorkerRun, AgentErrorKind> {
292    run_architect_worker_once_with_llm_vendor_vault(store, output_root, None)
293}
294
295/// Runs one bounded Architect worker iteration with optional external LLM vendor vault access.
296pub fn run_architect_worker_once_with_llm_vendor_vault(
297    store: &pharos_runtime_architect::database::ArchitectStore,
298    output_root: &Path,
299    llm_vendor_vault: Option<ArchitectLlmVendorVaultPaths>,
300) -> Result<ArchitectWorkerRun, AgentErrorKind> {
301    validate_worker_output_root(output_root)?;
302    pharos_runtime_architect::worker_runtime::run_architect_worker_once_with_services(
303        store,
304        output_root,
305        llm_vendor_vault,
306        &RootArchitectLlmSecretProvider,
307    )
308}
309
310/// Runs a bounded Architect worker loop for service supervisors.
311pub fn run_architect_worker_loop(
312    store: &pharos_runtime_architect::database::ArchitectStore,
313    output_root: &Path,
314    max_iterations: usize,
315) -> Result<ArchitectWorkerLoopRun, AgentErrorKind> {
316    validate_worker_output_root(output_root)?;
317    pharos_runtime_architect::worker_runtime::run_architect_worker_loop_with_services(
318        store,
319        output_root,
320        max_iterations,
321        &RootArchitectLlmSecretProvider,
322    )
323}
324
325/// Runs one Architect task from browser submission through local app validation.
326pub fn run_architect_task_to_validated(
327    store: &pharos_runtime_architect::database::ArchitectStore,
328    task_id: i64,
329    output_root: &Path,
330    llm_vendor_vault: Option<ArchitectLlmVendorVaultPaths>,
331) -> Result<ArchitectTaskApplyResult, AgentErrorKind> {
332    if task_id <= 0 {
333        return Err(AgentErrorKind::InvalidIdentifier);
334    }
335    validate_worker_output_root(output_root)?;
336    plan_architect_task_for_auto_build(store, task_id, output_root, llm_vendor_vault)?;
337    approve_architect_task_plan(store, task_id, output_root)
338}
339
340/// Queues a new Architect build task or resets the latest failed same-app task for a clean retry.
341pub fn queue_or_retry_architect_task(
342    store: &pharos_runtime_architect::database::ArchitectStore,
343    output_app_id: &str,
344    llm_vendor_id: &str,
345    brief: &str,
346    actor_id: i64,
347) -> Result<i64, AgentErrorKind> {
348    pharos_runtime_architect::task_repository::queue_or_retry_task_for_actor(
349        store,
350        output_app_id,
351        llm_vendor_id,
352        brief,
353        actor_id,
354    )
355}
356
357fn plan_architect_task_for_auto_build(
358    store: &pharos_runtime_architect::database::ArchitectStore,
359    task_id: i64,
360    output_root: &Path,
361    llm_vendor_vault: Option<ArchitectLlmVendorVaultPaths>,
362) -> Result<(), AgentErrorKind> {
363    pharos_runtime_architect::worker_runtime::plan_architect_task_for_auto_build(
364        store,
365        task_id,
366        output_root,
367        llm_vendor_vault,
368        &RootArchitectLlmSecretProvider,
369    )
370}
371
372/// Applies one browser-approved Architect task plan and runs local lifecycle validation.
373pub fn approve_architect_task_plan(
374    store: &pharos_runtime_architect::database::ArchitectStore,
375    task_id: i64,
376    generated_root: &Path,
377) -> Result<ArchitectTaskApplyResult, AgentErrorKind> {
378    if task_id <= 0 || generated_root.as_os_str().is_empty() {
379        return Err(AgentErrorKind::InvalidPath);
380    }
381    let task = pharos_runtime_architect::task_repository::load_review_task(store, task_id)?;
382    pharos_runtime_architect::task_repository::transition_task(
383        store,
384        task_id,
385        "applying",
386        "apply_started",
387        "Architect started validating the approved plan.",
388        std::collections::BTreeMap::new(),
389    )?;
390    let _dir_guard = architect_current_dir_lock()
391        .lock()
392        .map_err(|_| AgentErrorKind::PolicyViolation)?;
393    let mut lifecycle_tools = RootArchitectLifecycleTools {
394        connection_task: Some((store, task_id)),
395    };
396    let result = architect_apply_and_validate_review_workspace(
397        &task,
398        task_id,
399        generated_root,
400        store.config(),
401        &mut lifecycle_tools,
402    );
403    match result {
404        Ok(result) => {
405            pharos_runtime_architect::task_repository::mark_apply_success(store, task_id, &result)?;
406            Ok(result)
407        }
408        Err(error) => {
409            let _ = pharos_runtime_architect::task_repository::transition_task(
410                store,
411                task_id,
412                "failed",
413                "apply_failed",
414                "Architect could not validate the approved plan.",
415                std::collections::BTreeMap::new(),
416            );
417            Err(error)
418        }
419    }
420}
421
422/// Applies one approved planned source edit through the generic Architect store.
423pub fn apply_architect_editor_source_edit(
424    store: &pharos_runtime_architect::database::ArchitectStore,
425    change_id: i64,
426    generated_root: &Path,
427    actor_id: i64,
428) -> Result<ArchitectEditorSourceEditApplyResult, AgentErrorKind> {
429    apply_architect_editor_source_edit_inner(store, change_id, generated_root, actor_id, false)
430}
431
432/// Applies one approved planned source edit with an injected post-validation failure for local rollback proof.
433pub fn apply_architect_editor_source_edit_with_injected_failure(
434    store: &pharos_runtime_architect::database::ArchitectStore,
435    change_id: i64,
436    generated_root: &Path,
437    actor_id: i64,
438) -> Result<ArchitectEditorSourceEditApplyResult, AgentErrorKind> {
439    apply_architect_editor_source_edit_inner(store, change_id, generated_root, actor_id, true)
440}
441
442pub fn validate_existing_architect_task_source(
443    store: &pharos_runtime_architect::database::ArchitectStore,
444    task_id: i64,
445    generated_root: &Path,
446    actor_id: i64,
447) -> Result<(), AgentErrorKind> {
448    let mut tools = RootArchitectLifecycleTools {
449        connection_task: None,
450    };
451    pharos_runtime_architect::editor_runtime::validate_existing_task_source(
452        store,
453        task_id,
454        generated_root,
455        actor_id,
456        &mut tools,
457    )
458}
459
460fn apply_architect_editor_source_edit_inner(
461    store: &pharos_runtime_architect::database::ArchitectStore,
462    change_id: i64,
463    generated_root: &Path,
464    actor_id: i64,
465    inject_failure_after_validation: bool,
466) -> Result<ArchitectEditorSourceEditApplyResult, AgentErrorKind> {
467    let mut lifecycle_tools = RootArchitectLifecycleTools {
468        connection_task: None,
469    };
470    pharos_runtime_architect::editor_runtime::apply_architect_editor_source_edit_with_services(
471        store,
472        change_id,
473        generated_root,
474        actor_id,
475        inject_failure_after_validation,
476        &mut lifecycle_tools,
477    )
478}
479
480/// Marks one task as ready for browser review after deterministic synthesis and change planning.
481/// Validates one generated app id before resolving a browser preview path.
482pub fn normalize_architect_preview_app_id(value: &str) -> Result<String, AgentErrorKind> {
483    agent_bridge::normalize_app_id(value)
484}
485
486/// Converts bytes into lower-case hex text.
487fn validate_worker_output_root(output_root: &Path) -> Result<(), AgentErrorKind> {
488    if output_root.as_os_str().is_empty() {
489        return Err(AgentErrorKind::InvalidPath);
490    }
491    if let Ok(repo_root) = resolve_repo_root() {
492        let output_relative = repo_relative_path(&repo_root, output_root)?;
493        return match agent_bridge::classify_repo_path(&output_relative)? {
494            PathClassification::Artifact => Err(AgentErrorKind::PolicyViolation),
495            PathClassification::Source | PathClassification::Temp => Ok(()),
496        };
497    }
498    validate_installed_state_path(output_root)
499}
500
501fn validate_installed_state_path(path: &Path) -> Result<(), AgentErrorKind> {
502    if !path.is_absolute()
503        || path
504            .components()
505            .any(|component| matches!(component, Component::ParentDir))
506    {
507        return Err(AgentErrorKind::InvalidPath);
508    }
509    let parent = path.parent().ok_or(AgentErrorKind::InvalidPath)?;
510    let canonical_parent = parent
511        .canonicalize()
512        .map_err(|_| AgentErrorKind::InvalidPath)?;
513    if canonical_parent.starts_with("/var/state/pharos")
514        || canonical_parent.starts_with("/var/lib/pharos")
515    {
516        Ok(())
517    } else {
518        Err(AgentErrorKind::PolicyViolation)
519    }
520}
521
522/// Resolves the current Pharos repository root for Architect worker path checks.
523fn resolve_repo_root() -> Result<PathBuf, AgentErrorKind> {
524    let cwd = std::env::current_dir().map_err(|_| AgentErrorKind::InvalidPath)?;
525    let mut current = Some(cwd.as_path());
526    while let Some(path) = current {
527        if path.join("apps").is_dir() && path.join("scripts").is_dir() {
528            return path.canonicalize().map_err(|_| AgentErrorKind::InvalidPath);
529        }
530        current = path.parent();
531    }
532    Err(AgentErrorKind::InvalidPath)
533}
534
535/// Converts an output path into a repository-relative, traversal-free string.
536fn repo_relative_path(repo_root: &Path, path: &Path) -> Result<String, AgentErrorKind> {
537    let absolute = if path.is_absolute() {
538        path.to_path_buf()
539    } else {
540        repo_root.join(path)
541    };
542    if absolute
543        .components()
544        .any(|component| matches!(component, Component::ParentDir))
545    {
546        return Err(AgentErrorKind::InvalidPath);
547    }
548    let parent = absolute.parent().ok_or(AgentErrorKind::InvalidPath)?;
549    let canonical_parent = parent
550        .canonicalize()
551        .map_err(|_| AgentErrorKind::InvalidPath)?;
552    if !canonical_parent.starts_with(repo_root) {
553        return Err(AgentErrorKind::InvalidPath);
554    }
555    let normalized =
556        canonical_parent.join(absolute.file_name().ok_or(AgentErrorKind::InvalidPath)?);
557    let relative = normalized
558        .strip_prefix(repo_root)
559        .map_err(|_| AgentErrorKind::InvalidPath)?;
560    Ok(relative.to_string_lossy().into_owned())
561}