Skip to main content

agent_config/agents/
openclaw.rs

1//! OpenClaw integration.
2//!
3//! Implemented surfaces:
4//!
5//! 1. **Prompt rules**: project-local fenced blocks in `AGENTS.md`.
6//! 2. **Skills**: AgentSkills-compatible folders at `~/.openclaw/skills`
7//!    (Global) or `<root>/.agents/skills` (Local).
8//! 3. **MCP servers**: global OpenClaw JSON5 config at
9//!    `~/.openclaw/openclaw.json`, under `mcp.servers.<name>`.
10//!
11//! Native OpenClaw plugin and hook-pack installation remains intentionally
12//! deferred because upstream documents that as a CLI/plugin lifecycle rather
13//! than a stable file-backed hook contract.
14
15use std::collections::BTreeMap;
16use std::path::{Path, PathBuf};
17
18use serde_json::{Map, Value};
19
20use crate::agents::planning as agent_planning;
21use crate::error::AgentConfigError;
22use crate::integration::{
23    InstallReport, InstructionSurface, Integration, McpSurface, SkillSurface, UninstallReport,
24};
25use crate::paths;
26use crate::plan::{InstallPlan, UninstallPlan};
27use crate::scope::{Scope, ScopeKind};
28use crate::spec::{HookSpec, InstructionSpec, McpSpec, McpTransport, SkillSpec};
29use crate::status::StatusReport;
30use crate::util::{
31    file_lock, fs_atomic, instructions_dir, mcp_json_map, md_block, ownership, safe_fs, skills_dir,
32};
33
34/// OpenClaw file-backed installer.
35#[derive(Debug, Clone, Copy, Default)]
36pub struct OpenClawAgent {
37    _private: (),
38}
39
40impl OpenClawAgent {
41    /// Construct an instance. Stateless.
42    pub const fn new() -> Self {
43        Self { _private: () }
44    }
45
46    fn require_local(scope: &Scope) -> Result<&Path, AgentConfigError> {
47        match scope {
48            Scope::Local(p) => Ok(p),
49            Scope::Global => Err(AgentConfigError::UnsupportedScope {
50                id: "openclaw",
51                scope: ScopeKind::Global,
52            }),
53        }
54    }
55
56    fn prompt_path(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
57        Ok(Self::require_local(scope)?.join("AGENTS.md"))
58    }
59
60    fn openclaw_home_from_home(home: &Path) -> PathBuf {
61        home.join(".openclaw")
62    }
63
64    fn mcp_config_path_from_home(home: &Path) -> PathBuf {
65        Self::openclaw_home_from_home(home).join("openclaw.json")
66    }
67
68    fn mcp_path(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
69        match scope {
70            Scope::Global => Ok(Self::mcp_config_path_from_home(&paths::home_dir()?)),
71            Scope::Local(_) => Err(AgentConfigError::UnsupportedScope {
72                id: "openclaw",
73                scope: ScopeKind::Local,
74            }),
75        }
76    }
77
78    fn skills_root_from_home(home: &Path) -> PathBuf {
79        Self::openclaw_home_from_home(home).join("skills")
80    }
81
82    fn skills_root(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
83        Ok(match scope {
84            Scope::Global => Self::skills_root_from_home(&paths::home_dir()?),
85            Scope::Local(p) => p.join(".agents").join("skills"),
86        })
87    }
88
89    /// Directory holding the instruction ownership ledger. Local-only;
90    /// reuses the existing `<root>/.agents/` skills directory so the
91    /// sidecar lives next to other agent-config artifacts.
92    fn instruction_config_dir(scope: &Scope) -> Result<PathBuf, AgentConfigError> {
93        Ok(Self::require_local(scope)?.join(".agents"))
94    }
95
96    fn install_mcp_config(
97        config_path: &Path,
98        spec: &McpSpec,
99    ) -> Result<InstallReport, AgentConfigError> {
100        let ledger = ownership::mcp_ledger_for(config_path);
101        mcp_json_map::install(
102            config_path,
103            &ledger,
104            spec,
105            &["mcp", "servers"],
106            openclaw_mcp_value,
107            mcp_json_map::ConfigFormat::Json5,
108        )
109    }
110
111    fn uninstall_mcp_config(
112        config_path: &Path,
113        name: &str,
114        owner_tag: &str,
115    ) -> Result<UninstallReport, AgentConfigError> {
116        let ledger = ownership::mcp_ledger_for(config_path);
117        mcp_json_map::uninstall(
118            config_path,
119            &ledger,
120            name,
121            owner_tag,
122            "mcp server",
123            &["mcp", "servers"],
124            mcp_json_map::ConfigFormat::Json5,
125        )
126    }
127}
128
129impl Integration for OpenClawAgent {
130    fn id(&self) -> &'static str {
131        "openclaw"
132    }
133
134    fn display_name(&self) -> &'static str {
135        "OpenClaw"
136    }
137
138    fn supported_scopes(&self) -> &'static [ScopeKind] {
139        &[ScopeKind::Local]
140    }
141
142    fn status(&self, scope: &Scope, tag: &str) -> Result<StatusReport, AgentConfigError> {
143        HookSpec::validate_tag(tag)?;
144        let path = Self::prompt_path(scope)?;
145        StatusReport::for_markdown_block_hook(tag, path)
146    }
147
148    fn plan_install(
149        &self,
150        scope: &Scope,
151        spec: &HookSpec,
152    ) -> Result<InstallPlan, AgentConfigError> {
153        agent_planning::markdown_install(
154            Integration::id(self),
155            scope,
156            spec,
157            Self::prompt_path(scope),
158            true,
159        )
160    }
161
162    fn plan_uninstall(&self, scope: &Scope, tag: &str) -> Result<UninstallPlan, AgentConfigError> {
163        agent_planning::markdown_uninstall(
164            Integration::id(self),
165            scope,
166            tag,
167            Self::prompt_path(scope),
168        )
169    }
170
171    fn install(&self, scope: &Scope, spec: &HookSpec) -> Result<InstallReport, AgentConfigError> {
172        HookSpec::validate_tag(&spec.tag)?;
173        agent_planning::validate_prompt_only_event(Integration::id(self), &spec.event)?;
174        let rules = spec
175            .rules
176            .as_ref()
177            .ok_or(AgentConfigError::MissingSpecField {
178                id: "openclaw",
179                field: "rules",
180            })?;
181        let path = Self::prompt_path(scope)?;
182        let mut report = InstallReport::default();
183        scope.ensure_contained(&path)?;
184        file_lock::with_lock(&path, || {
185            let host = fs_atomic::read_to_string_or_empty(&path)?;
186            let new_host = md_block::upsert(&host, &spec.tag, &rules.content);
187            let outcome = safe_fs::write(scope, &path, new_host.as_bytes(), true)?;
188            if outcome.no_change {
189                report.already_installed = true;
190            } else if outcome.existed {
191                report.patched.push(outcome.path.clone());
192            } else {
193                report.created.push(outcome.path.clone());
194            }
195            if let Some(b) = outcome.backup {
196                report.backed_up.push(b);
197            }
198            Ok::<(), AgentConfigError>(())
199        })?;
200        Ok(report)
201    }
202
203    fn uninstall(&self, scope: &Scope, tag: &str) -> Result<UninstallReport, AgentConfigError> {
204        HookSpec::validate_tag(tag)?;
205        let path = Self::prompt_path(scope)?;
206        let mut report = UninstallReport::default();
207        scope.ensure_contained(&path)?;
208        file_lock::with_lock(&path, || {
209            let host = fs_atomic::read_to_string_or_empty(&path)?;
210            let (stripped, removed) = md_block::remove(&host, tag);
211
212            if !removed {
213                report.not_installed = true;
214                return Ok(());
215            }
216
217            if stripped.trim().is_empty() {
218                if safe_fs::restore_backup_if_matches(scope, &path, stripped.as_bytes())? {
219                    report.restored.push(path.clone());
220                } else {
221                    safe_fs::remove_file(scope, &path)?;
222                    report.removed.push(path.clone());
223                }
224            } else {
225                safe_fs::write(scope, &path, stripped.as_bytes(), false)?;
226                report.patched.push(path.clone());
227            }
228            Ok::<(), AgentConfigError>(())
229        })?;
230        Ok(report)
231    }
232}
233
234impl McpSurface for OpenClawAgent {
235    fn id(&self) -> &'static str {
236        "openclaw"
237    }
238
239    fn supported_mcp_scopes(&self) -> &'static [ScopeKind] {
240        &[ScopeKind::Global]
241    }
242
243    fn mcp_status(
244        &self,
245        scope: &Scope,
246        name: &str,
247        expected_owner: &str,
248    ) -> Result<StatusReport, AgentConfigError> {
249        McpSpec::validate_name(name)?;
250        let cfg = Self::mcp_path(scope)?;
251        let ledger = ownership::mcp_ledger_for(&cfg);
252        let presence = mcp_json_map::config_presence(
253            &cfg,
254            &["mcp", "servers"],
255            name,
256            mcp_json_map::ConfigFormat::Json5,
257        )?;
258        let recorded = ownership::owner_of(&ledger, name)?;
259        Ok(StatusReport::for_mcp(
260            name,
261            cfg,
262            ledger,
263            presence,
264            expected_owner,
265            recorded,
266        ))
267    }
268
269    fn plan_install_mcp(
270        &self,
271        scope: &Scope,
272        spec: &McpSpec,
273    ) -> Result<InstallPlan, AgentConfigError> {
274        agent_planning::mcp_json_map_install(
275            McpSurface::id(self),
276            scope,
277            spec,
278            Self::mcp_path(scope),
279            &["mcp", "servers"],
280            openclaw_mcp_value,
281            mcp_json_map::ConfigFormat::Json5,
282        )
283    }
284
285    fn plan_uninstall_mcp(
286        &self,
287        scope: &Scope,
288        name: &str,
289        owner_tag: &str,
290    ) -> Result<UninstallPlan, AgentConfigError> {
291        agent_planning::mcp_json_map_uninstall(
292            McpSurface::id(self),
293            scope,
294            name,
295            owner_tag,
296            Self::mcp_path(scope),
297            &["mcp", "servers"],
298            mcp_json_map::ConfigFormat::Json5,
299        )
300    }
301
302    fn is_mcp_installed(&self, scope: &Scope, name: &str) -> Result<bool, AgentConfigError> {
303        McpSpec::validate_name(name)?;
304        let cfg = Self::mcp_path(scope)?;
305        let ledger = ownership::mcp_ledger_for(&cfg);
306        mcp_json_map::is_installed(&ledger, name)
307    }
308
309    fn install_mcp(
310        &self,
311        scope: &Scope,
312        spec: &McpSpec,
313    ) -> Result<InstallReport, AgentConfigError> {
314        spec.validate()?;
315        let cfg = Self::mcp_path(scope)?;
316        spec.validate_local_secret_policy(scope)?;
317        scope.ensure_contained(&cfg)?;
318        Self::install_mcp_config(&cfg, spec)
319    }
320
321    fn uninstall_mcp(
322        &self,
323        scope: &Scope,
324        name: &str,
325        owner_tag: &str,
326    ) -> Result<UninstallReport, AgentConfigError> {
327        McpSpec::validate_name(name)?;
328        HookSpec::validate_tag(owner_tag)?;
329        let cfg = Self::mcp_path(scope)?;
330        scope.ensure_contained(&cfg)?;
331        Self::uninstall_mcp_config(&cfg, name, owner_tag)
332    }
333}
334
335impl SkillSurface for OpenClawAgent {
336    fn id(&self) -> &'static str {
337        "openclaw"
338    }
339
340    fn supported_skill_scopes(&self) -> &'static [ScopeKind] {
341        &[ScopeKind::Global, ScopeKind::Local]
342    }
343
344    fn skill_status(
345        &self,
346        scope: &Scope,
347        name: &str,
348        expected_owner: &str,
349    ) -> Result<StatusReport, AgentConfigError> {
350        SkillSpec::validate_name(name)?;
351        let root = Self::skills_root(scope)?;
352        let (dir, manifest, ledger) = skills_dir::paths_for_status(&root, name);
353        let recorded = ownership::owner_of(&ledger, name)?;
354        Ok(StatusReport::for_skill(
355            name,
356            dir,
357            manifest,
358            ledger,
359            expected_owner,
360            recorded,
361        ))
362    }
363
364    fn plan_install_skill(
365        &self,
366        scope: &Scope,
367        spec: &SkillSpec,
368    ) -> Result<InstallPlan, AgentConfigError> {
369        agent_planning::skill_install(
370            SkillSurface::id(self),
371            scope,
372            spec,
373            Self::skills_root(scope),
374        )
375    }
376
377    fn plan_uninstall_skill(
378        &self,
379        scope: &Scope,
380        name: &str,
381        owner_tag: &str,
382    ) -> Result<UninstallPlan, AgentConfigError> {
383        agent_planning::skill_uninstall(
384            SkillSurface::id(self),
385            scope,
386            name,
387            owner_tag,
388            Self::skills_root(scope),
389        )
390    }
391
392    fn is_skill_installed(&self, scope: &Scope, name: &str) -> Result<bool, AgentConfigError> {
393        let root = Self::skills_root(scope)?;
394        skills_dir::is_installed(&root, name)
395    }
396
397    fn install_skill(
398        &self,
399        scope: &Scope,
400        spec: &SkillSpec,
401    ) -> Result<InstallReport, AgentConfigError> {
402        let root = Self::skills_root(scope)?;
403        scope.ensure_contained(&root)?;
404        skills_dir::install(&root, spec)
405    }
406
407    fn uninstall_skill(
408        &self,
409        scope: &Scope,
410        name: &str,
411        owner_tag: &str,
412    ) -> Result<UninstallReport, AgentConfigError> {
413        let root = Self::skills_root(scope)?;
414        scope.ensure_contained(&root)?;
415        skills_dir::uninstall(&root, name, owner_tag)
416    }
417}
418
419impl OpenClawAgent {
420    fn inline_layout(
421        &self,
422        scope: &Scope,
423    ) -> Result<instructions_dir::InlineLayout, AgentConfigError> {
424        Ok(instructions_dir::InlineLayout {
425            config_dir: Self::instruction_config_dir(scope)?,
426            host_file: Self::prompt_path(scope)?,
427        })
428    }
429}
430
431impl InstructionSurface for OpenClawAgent {
432    fn id(&self) -> &'static str {
433        "openclaw"
434    }
435
436    fn supported_instruction_scopes(&self) -> &'static [ScopeKind] {
437        &[ScopeKind::Local]
438    }
439
440    fn instruction_status(
441        &self,
442        scope: &Scope,
443        name: &str,
444        expected_owner: &str,
445    ) -> Result<StatusReport, AgentConfigError> {
446        instructions_dir::inline_status(self.inline_layout(scope)?, name, expected_owner)
447    }
448
449    fn plan_install_instruction(
450        &self,
451        scope: &Scope,
452        spec: &InstructionSpec,
453    ) -> Result<InstallPlan, AgentConfigError> {
454        instructions_dir::inline_plan_install(
455            InstructionSurface::id(self),
456            scope,
457            self.inline_layout(scope),
458            spec,
459        )
460    }
461
462    fn plan_uninstall_instruction(
463        &self,
464        scope: &Scope,
465        name: &str,
466        owner_tag: &str,
467    ) -> Result<UninstallPlan, AgentConfigError> {
468        instructions_dir::inline_plan_uninstall(
469            InstructionSurface::id(self),
470            scope,
471            self.inline_layout(scope),
472            name,
473            owner_tag,
474        )
475    }
476
477    fn install_instruction(
478        &self,
479        scope: &Scope,
480        spec: &InstructionSpec,
481    ) -> Result<InstallReport, AgentConfigError> {
482        instructions_dir::inline_install(scope, self.inline_layout(scope)?, spec)
483    }
484
485    fn uninstall_instruction(
486        &self,
487        scope: &Scope,
488        name: &str,
489        owner_tag: &str,
490    ) -> Result<UninstallReport, AgentConfigError> {
491        instructions_dir::inline_uninstall(scope, self.inline_layout(scope)?, name, owner_tag)
492    }
493}
494
495fn openclaw_mcp_value(spec: &McpSpec) -> Value {
496    let mut obj = Map::new();
497    match &spec.transport {
498        McpTransport::Stdio { command, args, env } => {
499            obj.insert("command".into(), Value::String(command.clone()));
500            obj.insert(
501                "args".into(),
502                Value::Array(args.iter().cloned().map(Value::String).collect()),
503            );
504            if !env.is_empty() {
505                obj.insert("env".into(), string_map_value(env));
506            }
507        }
508        McpTransport::Http { url, headers } => {
509            obj.insert("url".into(), Value::String(url.clone()));
510            obj.insert("transport".into(), Value::String("streamable-http".into()));
511            if !headers.is_empty() {
512                obj.insert("headers".into(), string_map_value(headers));
513            }
514        }
515        McpTransport::Sse { url, headers } => {
516            obj.insert("url".into(), Value::String(url.clone()));
517            obj.insert("transport".into(), Value::String("sse".into()));
518            if !headers.is_empty() {
519                obj.insert("headers".into(), string_map_value(headers));
520            }
521        }
522    }
523    Value::Object(obj)
524}
525
526fn string_map_value(map: &BTreeMap<String, String>) -> Value {
527    let mut obj = Map::new();
528    for (k, v) in map {
529        obj.insert(k.clone(), Value::String(v.clone()));
530    }
531    Value::Object(obj)
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537    use serde_json::json;
538    use tempfile::tempdir;
539
540    fn rules_spec(tag: &str, rules: &str) -> HookSpec {
541        HookSpec::builder(tag)
542            .command_program("noop", [] as [&str; 0])
543            .rules(rules)
544            .build()
545    }
546
547    fn skill(name: &str, owner: &str) -> SkillSpec {
548        SkillSpec::builder(name)
549            .owner(owner)
550            .description("A test OpenClaw skill.")
551            .body("## Goal\nDo the thing.\n")
552            .build()
553    }
554
555    fn mcp_spec(name: &str, owner: &str) -> McpSpec {
556        McpSpec::builder(name)
557            .owner(owner)
558            .stdio("npx", ["-y", "@example/server"])
559            .env("FOO", "bar")
560            .build()
561    }
562
563    #[test]
564    fn install_rules_writes_agents_md_block() {
565        let dir = tempdir().unwrap();
566        let agent = OpenClawAgent::new();
567        let scope = Scope::Local(dir.path().to_path_buf());
568
569        agent
570            .install(&scope, &rules_spec("alpha", "Use the test rules."))
571            .unwrap();
572
573        let body = std::fs::read_to_string(dir.path().join("AGENTS.md")).unwrap();
574        assert!(body.contains("BEGIN AGENT-CONFIG:alpha"));
575        assert!(body.contains("Use the test rules."));
576        assert!(agent.is_installed(&scope, "alpha").unwrap());
577    }
578
579    #[test]
580    fn rules_install_is_idempotent() {
581        let dir = tempdir().unwrap();
582        let agent = OpenClawAgent::new();
583        let scope = Scope::Local(dir.path().to_path_buf());
584        let spec = rules_spec("alpha", "rules");
585
586        agent.install(&scope, &spec).unwrap();
587        let second = agent.install(&scope, &spec).unwrap();
588        assert!(second.already_installed);
589    }
590
591    #[test]
592    fn uninstall_rules_round_trip() {
593        let dir = tempdir().unwrap();
594        let agent = OpenClawAgent::new();
595        let scope = Scope::Local(dir.path().to_path_buf());
596
597        agent
598            .install(&scope, &rules_spec("alpha", "rules"))
599            .unwrap();
600        let report = agent.uninstall(&scope, "alpha").unwrap();
601        assert!(!report.removed.is_empty());
602        assert!(!dir.path().join("AGENTS.md").exists());
603    }
604
605    #[test]
606    fn install_skill_writes_local_agents_skills() {
607        let dir = tempdir().unwrap();
608        let agent = OpenClawAgent::new();
609        let scope = Scope::Local(dir.path().to_path_buf());
610
611        agent
612            .install_skill(&scope, &skill("alpha-skill", "myapp"))
613            .unwrap();
614
615        assert!(dir
616            .path()
617            .join(".agents/skills/alpha-skill/SKILL.md")
618            .exists());
619    }
620
621    #[test]
622    fn global_skill_root_uses_openclaw_skills() {
623        let home = PathBuf::from("/tmp/home");
624        assert_eq!(
625            OpenClawAgent::skills_root_from_home(&home),
626            PathBuf::from("/tmp/home/.openclaw/skills")
627        );
628    }
629
630    #[test]
631    fn skill_owner_mismatch_is_refused() {
632        let dir = tempdir().unwrap();
633        let agent = OpenClawAgent::new();
634        let scope = Scope::Local(dir.path().to_path_buf());
635
636        agent
637            .install_skill(&scope, &skill("alpha-skill", "app-a"))
638            .unwrap();
639        let err = agent
640            .install_skill(&scope, &skill("alpha-skill", "app-b"))
641            .unwrap_err();
642        assert!(matches!(err, AgentConfigError::NotOwnedByCaller { .. }));
643    }
644
645    #[test]
646    fn install_mcp_reads_json5_and_preserves_user_entries() {
647        let dir = tempdir().unwrap();
648        let cfg = dir.path().join("openclaw.json");
649        std::fs::write(
650            &cfg,
651            r#"{
652  // existing OpenClaw config
653  mcp: {
654    servers: {
655      user: { url: 'https://example.com/mcp' },
656    },
657  },
658  plugins: { enabled: true },
659}
660"#,
661        )
662        .unwrap();
663
664        OpenClawAgent::install_mcp_config(&cfg, &mcp_spec("github", "myapp")).unwrap();
665
666        let parsed: Value = serde_json::from_slice(&std::fs::read(&cfg).unwrap()).unwrap();
667        assert_eq!(parsed["plugins"]["enabled"], json!(true));
668        assert_eq!(
669            parsed["mcp"]["servers"]["user"]["url"],
670            json!("https://example.com/mcp")
671        );
672        assert_eq!(parsed["mcp"]["servers"]["github"]["command"], json!("npx"));
673        assert_eq!(
674            parsed["mcp"]["servers"]["github"]["env"]["FOO"],
675            json!("bar")
676        );
677    }
678
679    #[test]
680    fn install_mcp_is_idempotent() {
681        let dir = tempdir().unwrap();
682        let cfg = dir.path().join("openclaw.json");
683        let spec = mcp_spec("github", "myapp");
684
685        OpenClawAgent::install_mcp_config(&cfg, &spec).unwrap();
686        let second = OpenClawAgent::install_mcp_config(&cfg, &spec).unwrap();
687        assert!(second.already_installed);
688    }
689
690    #[test]
691    fn uninstall_mcp_round_trip() {
692        let dir = tempdir().unwrap();
693        let cfg = dir.path().join("openclaw.json");
694
695        OpenClawAgent::install_mcp_config(&cfg, &mcp_spec("github", "myapp")).unwrap();
696        let report = OpenClawAgent::uninstall_mcp_config(&cfg, "github", "myapp").unwrap();
697        assert!(!report.removed.is_empty());
698        assert!(!cfg.exists());
699    }
700
701    #[test]
702    fn uninstall_mcp_owner_mismatch_is_refused() {
703        let dir = tempdir().unwrap();
704        let cfg = dir.path().join("openclaw.json");
705
706        OpenClawAgent::install_mcp_config(&cfg, &mcp_spec("github", "app-a")).unwrap();
707        let err = OpenClawAgent::uninstall_mcp_config(&cfg, "github", "app-b").unwrap_err();
708        assert!(matches!(err, AgentConfigError::NotOwnedByCaller { .. }));
709    }
710
711    #[test]
712    fn local_mcp_scope_is_rejected() {
713        let dir = tempdir().unwrap();
714        let agent = OpenClawAgent::new();
715        let scope = Scope::Local(dir.path().to_path_buf());
716        let err = agent
717            .install_mcp(&scope, &mcp_spec("github", "myapp"))
718            .unwrap_err();
719        assert!(matches!(
720            err,
721            AgentConfigError::UnsupportedScope {
722                scope: ScopeKind::Local,
723                ..
724            }
725        ));
726    }
727
728    #[test]
729    fn remote_mcp_mapping_uses_openclaw_transport_names() {
730        let http = McpSpec::builder("docs")
731            .owner("myapp")
732            .http("https://example.com/mcp")
733            .build();
734        let sse = McpSpec::builder("events")
735            .owner("myapp")
736            .sse("https://example.com/sse")
737            .build();
738
739        assert_eq!(
740            openclaw_mcp_value(&http)["transport"],
741            json!("streamable-http")
742        );
743        assert_eq!(openclaw_mcp_value(&sse)["transport"], json!("sse"));
744    }
745}