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
29pub 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 let schema = contract.schema;
69 Ok((config, adapter, schema))
70}
71
72pub 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
109pub 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
122pub 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
139pub 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
185pub 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 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
287pub 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
295pub 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
310pub 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
325pub 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
340pub 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
372pub 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
422pub 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
432pub 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
480pub fn normalize_architect_preview_app_id(value: &str) -> Result<String, AgentErrorKind> {
483 agent_bridge::normalize_app_id(value)
484}
485
486fn 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
522fn 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
535fn 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}