Skip to main content

construct/
lib.rs

1#![warn(clippy::all, clippy::pedantic)]
2#![allow(
3    clippy::assigning_clones,
4    clippy::bool_to_int_with_if,
5    clippy::case_sensitive_file_extension_comparisons,
6    clippy::cast_possible_wrap,
7    clippy::doc_markdown,
8    clippy::field_reassign_with_default,
9    clippy::float_cmp,
10    clippy::implicit_clone,
11    clippy::items_after_statements,
12    clippy::map_unwrap_or,
13    clippy::manual_let_else,
14    clippy::missing_errors_doc,
15    clippy::missing_panics_doc,
16    clippy::module_name_repetitions,
17    clippy::must_use_candidate,
18    clippy::new_without_default,
19    clippy::needless_pass_by_value,
20    clippy::needless_raw_string_hashes,
21    clippy::redundant_closure_for_method_calls,
22    clippy::return_self_not_must_use,
23    clippy::similar_names,
24    clippy::single_match_else,
25    clippy::struct_field_names,
26    clippy::too_many_lines,
27    clippy::uninlined_format_args,
28    clippy::unnecessary_cast,
29    clippy::unnecessary_lazy_evaluations,
30    clippy::unnecessary_literal_bound,
31    clippy::unnecessary_map_or,
32    clippy::unused_self,
33    clippy::cast_precision_loss,
34    clippy::unnecessary_wraps,
35    // ── Temporarily allowed while CI stabilizes ──
36    // These lints fire broadly across the initial-commit codebase and are
37    // slated for incremental cleanup in dedicated follow-up PRs. They are
38    // allow-listed here so `-D warnings` in CI does not block feature work.
39    clippy::cast_possible_truncation,
40    clippy::cast_sign_loss,
41    clippy::collapsible_if,
42    clippy::default_trait_access,
43    clippy::doc_overindented_list_items,
44    clippy::double_ended_iterator_last,
45    clippy::double_must_use,
46    clippy::empty_line_after_doc_comments,
47    clippy::format_push_string,
48    clippy::if_not_else,
49    clippy::implicit_hasher,
50    clippy::ip_constant,
51    clippy::len_without_is_empty,
52    clippy::manual_clamp,
53    clippy::manual_contains,
54    clippy::manual_string_new,
55    clippy::match_same_arms,
56    clippy::needless_borrow,
57    clippy::needless_continue,
58    clippy::needless_option_as_deref,
59    clippy::redundant_closure,
60    clippy::ref_option,
61    clippy::result_large_err,
62    clippy::struct_excessive_bools,
63    clippy::trivially_copy_pass_by_ref,
64    clippy::type_complexity,
65    clippy::unchecked_time_subtraction,
66    clippy::unnested_or_patterns,
67    clippy::unused_async,
68    clippy::unwrap_or_default,
69    clippy::used_underscore_binding,
70    clippy::vec_init_then_push,
71    dead_code
72)]
73
74use clap::Subcommand;
75use serde::{Deserialize, Serialize};
76
77pub mod agent;
78pub(crate) mod approval;
79pub(crate) mod auth;
80pub mod channels;
81pub(crate) mod cli_input;
82pub mod commands;
83pub mod config;
84pub(crate) mod cost;
85pub mod cron;
86pub(crate) mod daemon;
87pub(crate) mod doctor;
88pub mod gateway;
89pub(crate) mod hardware;
90pub(crate) mod health;
91pub(crate) mod heartbeat;
92pub mod hooks;
93pub mod i18n;
94pub(crate) mod identity;
95pub(crate) mod integrations;
96pub mod mcp_server;
97pub mod memory;
98pub(crate) mod migration;
99pub(crate) mod multimodal;
100pub mod nodes;
101pub mod observability;
102pub(crate) mod onboard;
103pub mod peripherals;
104pub mod providers;
105pub mod rag;
106pub mod runtime;
107pub(crate) mod security;
108pub(crate) mod service;
109pub mod sidecars;
110pub(crate) mod skills;
111pub mod sop;
112pub mod tools;
113pub(crate) mod trust;
114pub(crate) mod tunnel;
115pub(crate) mod util;
116pub mod verifiable_intent;
117
118#[cfg(feature = "plugins-wasm")]
119pub mod plugins;
120
121pub use config::Config;
122
123/// Gateway management subcommands
124#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
125pub enum GatewayCommands {
126    /// Start the gateway server (default if no subcommand specified)
127    #[command(long_about = "\
128Start the gateway server (webhooks, websockets).
129
130Runs the HTTP/WebSocket gateway that accepts incoming webhook events \
131and WebSocket connections. Bind address defaults to the values in \
132your config file (gateway.host / gateway.port).
133
134Examples:
135  construct gateway start              # use config defaults
136  construct gateway start -p 8080      # listen on port 8080
137  construct gateway start --host 0.0.0.0   # requires [gateway].allow_public_bind=true or a tunnel
138  construct gateway start -p 0         # random available port")]
139    Start {
140        /// Port to listen on (use 0 for random available port); defaults to config gateway.port
141        #[arg(short, long)]
142        port: Option<u16>,
143
144        /// Host to bind to; defaults to config gateway.host
145        /// Note: Binding to 0.0.0.0 requires `gateway.allow_public_bind = true` in config
146        #[arg(long)]
147        host: Option<String>,
148    },
149    /// Restart the gateway server
150    #[command(long_about = "\
151Restart the gateway server.
152
153Stops the running gateway if present, then starts a new instance \
154with the current configuration.
155
156Examples:
157  construct gateway restart            # restart with config defaults
158  construct gateway restart -p 8080    # restart on port 8080")]
159    Restart {
160        /// Port to listen on (use 0 for random available port); defaults to config gateway.port
161        #[arg(short, long)]
162        port: Option<u16>,
163
164        /// Host to bind to; defaults to config gateway.host
165        /// Note: Binding to 0.0.0.0 requires `gateway.allow_public_bind = true` in config
166        #[arg(long)]
167        host: Option<String>,
168    },
169    /// Show or generate the pairing code without restarting
170    #[command(long_about = "\
171Show or generate the gateway pairing code.
172
173Displays the pairing code for connecting new clients without \
174restarting the gateway. Requires the gateway to be running.
175
176With --new, generates a fresh pairing code even if the gateway \
177was previously paired (useful for adding additional clients).
178
179Examples:
180  construct gateway get-paircode       # show current pairing code
181  construct gateway get-paircode --new # generate a new pairing code")]
182    GetPaircode {
183        /// Generate a new pairing code (even if already paired)
184        #[arg(long)]
185        new: bool,
186    },
187}
188
189/// Service management subcommands
190#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
191pub enum ServiceCommands {
192    /// Install daemon service unit for auto-start and restart
193    Install,
194    /// Start daemon service
195    Start,
196    /// Stop daemon service
197    Stop,
198    /// Restart daemon service to apply latest config
199    Restart,
200    /// Check daemon service status
201    Status,
202    /// Uninstall daemon service unit
203    Uninstall,
204    /// Tail daemon service logs
205    Logs {
206        /// Number of lines to show (default: 50)
207        #[arg(short = 'n', long, default_value = "50")]
208        lines: usize,
209        /// Follow log output (like tail -f)
210        #[arg(short, long)]
211        follow: bool,
212    },
213}
214
215/// Channel management subcommands
216#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
217pub enum ChannelCommands {
218    /// List all configured channels
219    List,
220    /// Start all configured channels (handled in main.rs for async)
221    Start,
222    /// Run health checks for configured channels (handled in main.rs for async)
223    Doctor,
224    /// Add a new channel configuration
225    #[command(long_about = "\
226Add a new channel configuration.
227
228Provide the channel type and a JSON object with the required \
229configuration keys for that channel type.
230
231Supported types: telegram, discord, slack, whatsapp, matrix, imessage, email.
232
233Examples:
234  construct channel add telegram '{\"bot_token\":\"...\",\"name\":\"my-bot\"}'
235  construct channel add discord '{\"bot_token\":\"...\",\"name\":\"my-discord\"}'")]
236    Add {
237        /// Channel type (telegram, discord, slack, whatsapp, matrix, imessage, email)
238        channel_type: String,
239        /// Optional configuration as JSON
240        config: String,
241    },
242    /// Remove a channel configuration
243    Remove {
244        /// Channel name to remove
245        name: String,
246    },
247    /// Bind a Telegram identity (username or numeric user ID) into allowlist
248    #[command(long_about = "\
249Bind a Telegram identity into the allowlist.
250
251Adds a Telegram username (without the '@' prefix) or numeric user \
252ID to the channel allowlist so the agent will respond to messages \
253from that identity.
254
255Examples:
256  construct channel bind-telegram construct_user
257  construct channel bind-telegram 123456789")]
258    BindTelegram {
259        /// Telegram identity to allow (username without '@' or numeric user ID)
260        identity: String,
261    },
262    /// Send a message to a configured channel
263    #[command(long_about = "\
264Send a one-off message to a configured channel.
265
266Sends a text message through the specified channel without starting \
267the full agent loop. Useful for scripted notifications, hardware \
268sensor alerts, and automation pipelines.
269
270The --channel-id selects the channel by its config section name \
271(e.g. 'telegram', 'discord', 'slack'). The --recipient is the \
272platform-specific destination (e.g. a Telegram chat ID).
273
274Examples:
275  construct channel send 'Someone is near your device.' --channel-id telegram --recipient 123456789
276  construct channel send 'Build succeeded!' --channel-id discord --recipient 987654321")]
277    Send {
278        /// Message text to send
279        message: String,
280        /// Channel config name (e.g. telegram, discord, slack)
281        #[arg(long)]
282        channel_id: String,
283        /// Recipient identifier (platform-specific, e.g. Telegram chat ID)
284        #[arg(long)]
285        recipient: String,
286    },
287}
288
289/// Skills management subcommands
290#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
291pub enum SkillCommands {
292    /// List all installed skills
293    List,
294    /// Audit a skill source directory or installed skill name
295    Audit {
296        /// Skill path or installed skill name
297        source: String,
298    },
299    /// Install a new skill from a URL or local path
300    Install {
301        /// Source URL or local path
302        source: String,
303    },
304    /// Remove an installed skill
305    Remove {
306        /// Skill name to remove
307        name: String,
308    },
309    /// Run TEST.sh validation for a skill (or all skills)
310    Test {
311        /// Skill name to test; omit for all skills
312        name: Option<String>,
313        /// Show verbose output
314        #[arg(long)]
315        verbose: bool,
316    },
317}
318
319/// Workflow management subcommands
320#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
321pub enum WorkflowCommands {
322    /// List built-in workflows bundled with this binary.
323    List,
324    /// Seed (or refresh) built-in workflows into the workspace.
325    #[command(long_about = "\
326Copy the workflow YAMLs bundled with this binary into the active \
327workspace at `operator_mcp/workflow/builtins/`. Existing files are left \
328alone unless --force is passed.")]
329    Sync {
330        /// Overwrite workflow files even when they already exist on disk.
331        #[arg(long)]
332        force: bool,
333    },
334}
335
336/// Migration subcommands
337#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
338pub enum MigrateCommands {
339    /// Import memory from an `OpenClaw` workspace into this `Construct` workspace
340    Openclaw {
341        /// Optional path to `OpenClaw` workspace (defaults to ~/.openclaw/workspace)
342        #[arg(long)]
343        source: Option<std::path::PathBuf>,
344
345        /// Validate and preview migration without writing any data
346        #[arg(long)]
347        dry_run: bool,
348    },
349}
350
351/// Cron subcommands
352#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
353pub enum CronCommands {
354    /// List all scheduled tasks
355    List,
356    /// Add a new scheduled task
357    #[command(long_about = "\
358Add a new recurring scheduled task.
359
360Uses standard 5-field cron syntax: 'min hour day month weekday'. \
361Times are evaluated in UTC by default; use --tz with an IANA \
362timezone name to override.
363
364Examples:
365  construct cron add '0 9 * * 1-5' 'Good morning' --tz America/New_York --agent
366  construct cron add '*/30 * * * *' 'Check system health' --agent
367  construct cron add '*/5 * * * *' 'echo ok'")]
368    Add {
369        /// Cron expression
370        expression: String,
371        /// Optional IANA timezone (e.g. America/Los_Angeles)
372        #[arg(long)]
373        tz: Option<String>,
374        /// Treat the argument as an agent prompt instead of a shell command
375        #[arg(long)]
376        agent: bool,
377        /// Restrict agent cron jobs to the specified tool names (repeatable, agent-only)
378        #[arg(long = "allowed-tool")]
379        allowed_tools: Vec<String>,
380        /// Command (shell) or prompt (agent) to run
381        command: String,
382    },
383    /// Add a one-shot scheduled task at an RFC3339 timestamp
384    #[command(long_about = "\
385Add a one-shot task that fires at a specific UTC timestamp.
386
387The timestamp must be in RFC 3339 format (e.g. 2025-01-15T14:00:00Z).
388
389Examples:
390  construct cron add-at 2025-01-15T14:00:00Z 'Send reminder'
391  construct cron add-at 2025-12-31T23:59:00Z 'Happy New Year!'")]
392    AddAt {
393        /// One-shot timestamp in RFC3339 format
394        at: String,
395        /// Treat the argument as an agent prompt instead of a shell command
396        #[arg(long)]
397        agent: bool,
398        /// Restrict agent cron jobs to the specified tool names (repeatable, agent-only)
399        #[arg(long = "allowed-tool")]
400        allowed_tools: Vec<String>,
401        /// Command (shell) or prompt (agent) to run
402        command: String,
403    },
404    /// Add a fixed-interval scheduled task
405    #[command(long_about = "\
406Add a task that repeats at a fixed interval.
407
408Interval is specified in milliseconds. For example, 60000 = 1 minute.
409
410Examples:
411  construct cron add-every 60000 'Ping heartbeat'     # every minute
412  construct cron add-every 3600000 'Hourly report'    # every hour")]
413    AddEvery {
414        /// Interval in milliseconds
415        every_ms: u64,
416        /// Treat the argument as an agent prompt instead of a shell command
417        #[arg(long)]
418        agent: bool,
419        /// Restrict agent cron jobs to the specified tool names (repeatable, agent-only)
420        #[arg(long = "allowed-tool")]
421        allowed_tools: Vec<String>,
422        /// Command (shell) or prompt (agent) to run
423        command: String,
424    },
425    /// Add a one-shot delayed task (e.g. "30m", "2h", "1d")
426    #[command(long_about = "\
427Add a one-shot task that fires after a delay from now.
428
429Accepts human-readable durations: s (seconds), m (minutes), \
430h (hours), d (days).
431
432Examples:
433  construct cron once 30m 'Run backup in 30 minutes'
434  construct cron once 2h 'Follow up on deployment'
435  construct cron once 1d 'Daily check'")]
436    Once {
437        /// Delay duration
438        delay: String,
439        /// Treat the argument as an agent prompt instead of a shell command
440        #[arg(long)]
441        agent: bool,
442        /// Restrict agent cron jobs to the specified tool names (repeatable, agent-only)
443        #[arg(long = "allowed-tool")]
444        allowed_tools: Vec<String>,
445        /// Command (shell) or prompt (agent) to run
446        command: String,
447    },
448    /// Remove a scheduled task
449    Remove {
450        /// Task ID
451        id: String,
452    },
453    /// Update a scheduled task
454    #[command(long_about = "\
455Update one or more fields of an existing scheduled task.
456
457Only the fields you specify are changed; others remain unchanged.
458
459Examples:
460  construct cron update <task-id> --expression '0 8 * * *'
461  construct cron update <task-id> --tz Europe/London --name 'Morning check'
462  construct cron update <task-id> --command 'Updated message'")]
463    Update {
464        /// Task ID
465        id: String,
466        /// New cron expression
467        #[arg(long)]
468        expression: Option<String>,
469        /// New IANA timezone
470        #[arg(long)]
471        tz: Option<String>,
472        /// New command to run
473        #[arg(long)]
474        command: Option<String>,
475        /// New job name
476        #[arg(long)]
477        name: Option<String>,
478        /// Replace the agent job allowlist with the specified tool names (repeatable)
479        #[arg(long = "allowed-tool")]
480        allowed_tools: Vec<String>,
481    },
482    /// Pause a scheduled task
483    Pause {
484        /// Task ID
485        id: String,
486    },
487    /// Resume a paused task
488    Resume {
489        /// Task ID
490        id: String,
491    },
492}
493
494/// Memory management subcommands
495#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
496pub enum MemoryCommands {
497    /// List memory entries with optional filters
498    List {
499        /// Filter by category (core, daily, conversation, or custom name)
500        #[arg(long)]
501        category: Option<String>,
502        /// Filter by session ID
503        #[arg(long)]
504        session: Option<String>,
505        /// Maximum number of entries to display
506        #[arg(long, default_value = "50")]
507        limit: usize,
508        /// Number of entries to skip (for pagination)
509        #[arg(long, default_value = "0")]
510        offset: usize,
511    },
512    /// Get a specific memory entry by key
513    Get {
514        /// Memory key to look up
515        key: String,
516    },
517    /// Show memory backend statistics and health
518    Stats,
519    /// Clear memories by category, by key, or clear all
520    Clear {
521        /// Delete a single entry by key (supports prefix match)
522        #[arg(long)]
523        key: Option<String>,
524        /// Only clear entries in this category
525        #[arg(long)]
526        category: Option<String>,
527        /// Skip confirmation prompt
528        #[arg(long)]
529        yes: bool,
530    },
531}
532
533/// Integration subcommands
534#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
535pub enum IntegrationCommands {
536    /// Show details about a specific integration
537    Info {
538        /// Integration name
539        name: String,
540    },
541}
542
543/// Hardware discovery subcommands
544#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
545pub enum HardwareCommands {
546    /// Enumerate USB devices (VID/PID) and show known boards
547    #[command(long_about = "\
548Enumerate USB devices and show known boards.
549
550Scans connected USB devices by VID/PID and matches them against \
551known development boards (STM32 Nucleo, Arduino, ESP32).
552
553Examples:
554  construct hardware discover")]
555    Discover,
556    /// Introspect a device by path (e.g. /dev/ttyACM0)
557    #[command(long_about = "\
558Introspect a device by its serial or device path.
559
560Opens the specified device path and queries for board information, \
561firmware version, and supported capabilities.
562
563Examples:
564  construct hardware introspect /dev/ttyACM0
565  construct hardware introspect COM3")]
566    Introspect {
567        /// Serial or device path
568        path: String,
569    },
570    /// Get chip info via USB (probe-rs over ST-Link). No firmware needed on target.
571    #[command(long_about = "\
572Get chip info via USB using probe-rs over ST-Link.
573
574Queries the target MCU directly through the debug probe without \
575requiring any firmware on the target board.
576
577Examples:
578  construct hardware info
579  construct hardware info --chip STM32F401RETx")]
580    Info {
581        /// Chip name (e.g. STM32F401RETx). Default: STM32F401RETx for Nucleo-F401RE
582        #[arg(long, default_value = "STM32F401RETx")]
583        chip: String,
584    },
585}
586
587/// Peripheral (hardware) management subcommands
588#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
589pub enum PeripheralCommands {
590    /// List configured peripherals
591    List,
592    /// Add a peripheral (board path, e.g. nucleo-f401re /dev/ttyACM0)
593    #[command(long_about = "\
594Add a peripheral by board type and transport path.
595
596Registers a hardware board so the agent can use its tools (GPIO, \
597sensors, actuators). Use 'native' as path for local GPIO on \
598single-board computers like Raspberry Pi.
599
600Supported boards: nucleo-f401re, rpi-gpio, esp32, arduino-uno.
601
602Examples:
603  construct peripheral add nucleo-f401re /dev/ttyACM0
604  construct peripheral add rpi-gpio native
605  construct peripheral add esp32 /dev/ttyUSB0")]
606    Add {
607        /// Board type (nucleo-f401re, rpi-gpio, esp32)
608        board: String,
609        /// Path for serial transport (/dev/ttyACM0) or "native" for local GPIO
610        path: String,
611    },
612    /// Flash Construct firmware to Arduino (creates .ino, installs arduino-cli if needed, uploads)
613    #[command(long_about = "\
614Flash Construct firmware to an Arduino board.
615
616Generates the .ino sketch, installs arduino-cli if it is not \
617already available, compiles, and uploads the firmware.
618
619Examples:
620  construct peripheral flash
621  construct peripheral flash --port /dev/cu.usbmodem12345
622  construct peripheral flash -p COM3")]
623    Flash {
624        /// Serial port (e.g. /dev/cu.usbmodem12345). If omitted, uses first arduino-uno from config.
625        #[arg(short, long)]
626        port: Option<String>,
627    },
628    /// Setup Arduino Uno Q Bridge app (deploy GPIO bridge for agent control)
629    SetupUnoQ {
630        /// Uno Q IP (e.g. 192.168.0.48). If omitted, assumes running ON the Uno Q.
631        #[arg(long)]
632        host: Option<String>,
633    },
634    /// Flash Construct firmware to Nucleo-F401RE (builds + probe-rs run)
635    FlashNucleo,
636}
637
638/// SOP management subcommands
639#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
640pub enum SopCommands {
641    /// List loaded SOPs
642    List,
643    /// Validate SOP definitions
644    Validate {
645        /// SOP name to validate (all if omitted)
646        name: Option<String>,
647    },
648    /// Show details of an SOP
649    Show {
650        /// Name of the SOP to show
651        name: String,
652    },
653}