1use super::{Capability, CapabilityLocalization, CapabilityStatus};
22use crate::SessionId;
23use crate::connector::{
24 Connector, ConnectorFormSchema, ConnectorPlugin, ConnectorType, ConnectorValidation, FormField,
25};
26use crate::tool_types::ToolHints;
27use crate::tools::{Tool, ToolExecutionResult};
28use crate::traits::{SessionFileSystem, ToolContext};
29use async_trait::async_trait;
30use serde::{Deserialize, Serialize};
31use serde_json::{Value, json};
32use std::sync::OnceLock;
33use std::time::Duration;
34
35inventory::submit! {
36 ConnectorPlugin {
37 experimental_only: true,
38 factory: || Box::new(FakeAwsConnector),
39 }
40}
41
42pub struct FakeAwsConnector;
47
48#[async_trait]
49impl Connector for FakeAwsConnector {
50 fn provider_id(&self) -> &str {
51 "fake_aws"
52 }
53
54 fn display_name(&self) -> &str {
55 "Fake AWS"
56 }
57
58 fn description(&self) -> &str {
59 "Demo AWS-style connection for authenticated handoff examples."
60 }
61
62 fn icon(&self) -> &str {
63 "cloud"
64 }
65
66 fn connection_type(&self) -> ConnectorType {
67 ConnectorType::ApiKey
68 }
69
70 fn form_schema(&self) -> Option<ConnectorFormSchema> {
71 Some(ConnectorFormSchema {
72 fields: vec![
73 FormField::password("api_key", "Fake AWS API key")
74 .required()
75 .with_placeholder("fake_aws_...")
76 .with_help("Any non-empty value is accepted for local fake AWS testing."),
77 ],
78 instructions_markdown:
79 "Use any non-empty value for local fake AWS handoff testing. Real providers should validate credentials against their upstream API."
80 .to_string(),
81 })
82 }
83
84 async fn validate(&self, credential: &str) -> Result<ConnectorValidation, String> {
85 if credential.trim().is_empty() {
86 return Err("Fake AWS API key cannot be empty".to_string());
87 }
88 Ok(ConnectorValidation {
89 provider_username: Some("fake-aws-account".to_string()),
90 provider_metadata: Some(json!({
91 "account_alias": "fake-aws-account",
92 "provider": "fake_aws"
93 })),
94 })
95 }
96}
97
98fn base_latency_ms() -> u64 {
104 static BASE: OnceLock<u64> = OnceLock::new();
105 *BASE.get_or_init(|| {
106 std::env::var("FAKE_AWS_LATENCY_MS")
107 .ok()
108 .and_then(|v| v.parse().ok())
109 .unwrap_or(0)
110 })
111}
112
113#[derive(Clone, Copy)]
115enum OpKind {
116 List,
118 Create,
120 Modify,
122 Query,
124}
125
126impl OpKind {
127 fn multiplier(self) -> f64 {
128 match self {
129 OpKind::List => 1.0,
130 OpKind::Create => 3.0,
131 OpKind::Modify => 2.0,
132 OpKind::Query => 1.5,
133 }
134 }
135}
136
137fn jitter_factor() -> f64 {
139 let nanos = chrono::Utc::now().timestamp_subsec_nanos();
140 0.8 + (nanos % 400) as f64 / 1000.0
142}
143
144async fn simulate_latency(op: OpKind) {
146 let base = base_latency_ms();
147 if base == 0 {
148 return;
149 }
150 let ms = (base as f64 * op.multiplier() * jitter_factor()) as u64;
151 tokio::time::sleep(Duration::from_millis(ms)).await;
152}
153
154async fn read_or_seed<T: Serialize + for<'de> Deserialize<'de>>(
161 store: &dyn SessionFileSystem,
162 session_id: SessionId,
163 path: &str,
164 seed: impl FnOnce() -> Vec<T>,
165) -> Vec<T> {
166 if let Ok(Some(file)) = store.read_file(session_id, path).await
167 && let Ok(items) = serde_json::from_str::<Vec<T>>(file.content.as_deref().unwrap_or(""))
168 {
169 return items;
170 }
171 let items = seed();
173 if let Ok(json) = serde_json::to_string_pretty(&items) {
174 let _ = store.write_file(session_id, path, &json, "text").await;
175 }
176 items
177}
178
179async fn persist<T: Serialize>(
181 store: &dyn SessionFileSystem,
182 session_id: SessionId,
183 path: &str,
184 items: &[T],
185) -> Result<(), anyhow::Error> {
186 let json = serde_json::to_string_pretty(items)?;
187 store.write_file(session_id, path, &json, "text").await?;
188 Ok(())
189}
190
191use super::util::require_file_store;
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
198struct Ec2Instance {
199 instance_id: String,
200 instance_type: String,
201 state: String,
202 availability_zone: String,
203 private_ip: String,
204 public_ip: Option<String>,
205 launch_time: String,
206 tags: Vec<Tag>,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
210struct Tag {
211 key: String,
212 value: String,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
216struct RdsDatabase {
217 db_instance_id: String,
218 engine: String,
219 engine_version: String,
220 instance_class: String,
221 status: String,
222 endpoint: String,
223 port: i32,
224 storage_gb: i32,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228struct S3Bucket {
229 name: String,
230 region: String,
231 creation_date: String,
232 versioning_enabled: bool,
233 encryption_enabled: bool,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
237struct IamUser {
238 username: String,
239 user_id: String,
240 arn: String,
241 created_at: String,
242 permissions: Vec<String>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246struct SecurityGroup {
247 group_id: String,
248 group_name: String,
249 description: String,
250 vpc_id: String,
251 inbound_rules: Vec<SecurityRule>,
252 outbound_rules: Vec<SecurityRule>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
256struct SecurityRule {
257 protocol: String,
258 port_range: String,
259 source: String,
260}
261
262const EC2_PATH: &str = "/aws/ec2_instances.json";
267const RDS_PATH: &str = "/aws/rds_databases.json";
268const S3_PATH: &str = "/aws/s3_buckets.json";
269const IAM_PATH: &str = "/aws/iam_users.json";
270const SG_PATH: &str = "/aws/security_groups.json";
271
272fn seed_ec2() -> Vec<Ec2Instance> {
273 vec![
274 Ec2Instance {
276 instance_id: "i-0a1b2c3d4e5f60001".into(),
277 instance_type: "t3.medium".into(),
278 state: "running".into(),
279 availability_zone: "us-east-1a".into(),
280 private_ip: "10.0.1.10".into(),
281 public_ip: Some("54.200.10.1".into()),
282 launch_time: "2025-06-15T08:00:00Z".into(),
283 tags: vec![
284 Tag {
285 key: "Name".into(),
286 value: "web-server-01".into(),
287 },
288 Tag {
289 key: "Environment".into(),
290 value: "production".into(),
291 },
292 Tag {
293 key: "Team".into(),
294 value: "platform".into(),
295 },
296 ],
297 },
298 Ec2Instance {
299 instance_id: "i-0a1b2c3d4e5f60002".into(),
300 instance_type: "t3.medium".into(),
301 state: "running".into(),
302 availability_zone: "us-east-1b".into(),
303 private_ip: "10.0.2.10".into(),
304 public_ip: Some("54.200.10.2".into()),
305 launch_time: "2025-06-15T08:05:00Z".into(),
306 tags: vec![
307 Tag {
308 key: "Name".into(),
309 value: "web-server-02".into(),
310 },
311 Tag {
312 key: "Environment".into(),
313 value: "production".into(),
314 },
315 Tag {
316 key: "Team".into(),
317 value: "platform".into(),
318 },
319 ],
320 },
321 Ec2Instance {
322 instance_id: "i-0a1b2c3d4e5f60003".into(),
323 instance_type: "t3.large".into(),
324 state: "running".into(),
325 availability_zone: "us-east-1a".into(),
326 private_ip: "10.0.1.20".into(),
327 public_ip: Some("54.200.10.3".into()),
328 launch_time: "2025-07-01T10:00:00Z".into(),
329 tags: vec![
330 Tag {
331 key: "Name".into(),
332 value: "api-server-01".into(),
333 },
334 Tag {
335 key: "Environment".into(),
336 value: "production".into(),
337 },
338 Tag {
339 key: "Team".into(),
340 value: "backend".into(),
341 },
342 ],
343 },
344 Ec2Instance {
345 instance_id: "i-0a1b2c3d4e5f60004".into(),
346 instance_type: "t3.large".into(),
347 state: "running".into(),
348 availability_zone: "us-east-1b".into(),
349 private_ip: "10.0.2.20".into(),
350 public_ip: Some("54.200.10.4".into()),
351 launch_time: "2025-07-01T10:05:00Z".into(),
352 tags: vec![
353 Tag {
354 key: "Name".into(),
355 value: "api-server-02".into(),
356 },
357 Tag {
358 key: "Environment".into(),
359 value: "production".into(),
360 },
361 Tag {
362 key: "Team".into(),
363 value: "backend".into(),
364 },
365 ],
366 },
367 Ec2Instance {
369 instance_id: "i-0a1b2c3d4e5f60005".into(),
370 instance_type: "m5.xlarge".into(),
371 state: "running".into(),
372 availability_zone: "us-east-1a".into(),
373 private_ip: "10.0.1.30".into(),
374 public_ip: Some("54.200.10.5".into()),
375 launch_time: "2025-03-10T09:00:00Z".into(),
376 tags: vec![
377 Tag {
378 key: "Name".into(),
379 value: "batch-processor-01".into(),
380 },
381 Tag {
382 key: "Environment".into(),
383 value: "production".into(),
384 },
385 ],
386 },
387 Ec2Instance {
389 instance_id: "i-0a1b2c3d4e5f60006".into(),
390 instance_type: "m4.large".into(),
391 state: "running".into(),
392 availability_zone: "us-east-1a".into(),
393 private_ip: "10.0.1.40".into(),
394 public_ip: Some("54.200.10.6".into()),
395 launch_time: "2024-11-01T12:00:00Z".into(),
396 tags: vec![Tag {
397 key: "Name".into(),
398 value: "legacy-app-01".into(),
399 }],
400 },
401 Ec2Instance {
403 instance_id: "i-0a1b2c3d4e5f60007".into(),
404 instance_type: "t3.small".into(),
405 state: "running".into(),
406 availability_zone: "us-east-1a".into(),
407 private_ip: "10.0.1.50".into(),
408 public_ip: Some("54.200.10.7".into()),
409 launch_time: "2025-01-15T14:00:00Z".into(),
410 tags: vec![
411 Tag {
412 key: "Name".into(),
413 value: "test-server-01".into(),
414 },
415 Tag {
416 key: "Environment".into(),
417 value: "development".into(),
418 },
419 Tag {
420 key: "Owner".into(),
421 value: "intern-temp".into(),
422 },
423 ],
424 },
425 Ec2Instance {
427 instance_id: "i-0a1b2c3d4e5f60008".into(),
428 instance_type: "c5.2xlarge".into(),
429 state: "stopped".into(),
430 availability_zone: "us-east-1b".into(),
431 private_ip: "10.0.2.60".into(),
432 public_ip: None,
433 launch_time: "2025-02-01T08:00:00Z".into(),
434 tags: vec![],
435 },
436 ]
437}
438
439fn seed_rds() -> Vec<RdsDatabase> {
440 vec![
441 RdsDatabase {
443 db_instance_id: "prod-postgres-01".into(),
444 engine: "postgres".into(),
445 engine_version: "15.4".into(),
446 instance_class: "db.t3.medium".into(),
447 status: "available".into(),
448 endpoint: "prod-postgres-01.abc123.us-east-1.rds.amazonaws.com".into(),
449 port: 5432,
450 storage_gb: 100,
451 },
452 RdsDatabase {
454 db_instance_id: "staging-mysql-01".into(),
455 engine: "mysql".into(),
456 engine_version: "8.0".into(),
457 instance_class: "db.r5.large".into(),
458 status: "available".into(),
459 endpoint: "staging-mysql-01.abc123.us-east-1.rds.amazonaws.com".into(),
460 port: 3306,
461 storage_gb: 500,
462 },
463 RdsDatabase {
465 db_instance_id: "legacy-pg-11".into(),
466 engine: "postgres".into(),
467 engine_version: "11.22".into(),
468 instance_class: "db.t3.small".into(),
469 status: "available".into(),
470 endpoint: "legacy-pg-11.abc123.us-east-1.rds.amazonaws.com".into(),
471 port: 5432,
472 storage_gb: 20,
473 },
474 ]
475}
476
477fn seed_s3() -> Vec<S3Bucket> {
478 vec![
479 S3Bucket {
481 name: "company-data-backup".into(),
482 region: "us-east-1".into(),
483 creation_date: "2024-06-01T00:00:00Z".into(),
484 versioning_enabled: true,
485 encryption_enabled: true,
486 },
487 S3Bucket {
489 name: "static-assets-prod".into(),
490 region: "us-east-1".into(),
491 creation_date: "2024-08-15T00:00:00Z".into(),
492 versioning_enabled: false,
493 encryption_enabled: true,
494 },
495 S3Bucket {
497 name: "temp-data-dump".into(),
498 region: "us-east-1".into(),
499 creation_date: "2025-04-01T00:00:00Z".into(),
500 versioning_enabled: false,
501 encryption_enabled: false,
502 },
503 S3Bucket {
505 name: "customer-pii-records".into(),
506 region: "us-east-1".into(),
507 creation_date: "2024-09-10T00:00:00Z".into(),
508 versioning_enabled: true,
509 encryption_enabled: false,
510 },
511 S3Bucket {
513 name: "dev-test-artifacts".into(),
514 region: "us-west-2".into(),
515 creation_date: "2025-01-20T00:00:00Z".into(),
516 versioning_enabled: false,
517 encryption_enabled: false,
518 },
519 S3Bucket {
521 name: "log-archive-2024".into(),
522 region: "us-east-1".into(),
523 creation_date: "2024-01-01T00:00:00Z".into(),
524 versioning_enabled: true,
525 encryption_enabled: true,
526 },
527 ]
528}
529
530fn seed_iam() -> Vec<IamUser> {
531 vec![
532 IamUser {
534 username: "admin-user".into(),
535 user_id: "AIDAI23XYZABC123DEF".into(),
536 arn: "arn:aws:iam::123456789012:user/admin-user".into(),
537 created_at: "2024-01-15T10:00:00Z".into(),
538 permissions: vec!["AdministratorAccess".into()],
539 },
540 IamUser {
542 username: "developer-user".into(),
543 user_id: "AIDAI23XYZABC456GHI".into(),
544 arn: "arn:aws:iam::123456789012:user/developer-user".into(),
545 created_at: "2024-02-20T14:30:00Z".into(),
546 permissions: vec!["PowerUserAccess".into()],
547 },
548 IamUser {
550 username: "ci-deploy-bot".into(),
551 user_id: "AIDAI23XYZABC789JKL".into(),
552 arn: "arn:aws:iam::123456789012:user/ci-deploy-bot".into(),
553 created_at: "2024-06-01T09:00:00Z".into(),
554 permissions: vec!["AdministratorAccess".into(), "IAMFullAccess".into()],
555 },
556 IamUser {
558 username: "intern-temp".into(),
559 user_id: "AIDAI23XYZABCMNOPQR".into(),
560 arn: "arn:aws:iam::123456789012:user/intern-temp".into(),
561 created_at: "2025-06-01T10:00:00Z".into(),
562 permissions: vec![
563 "AmazonS3FullAccess".into(),
564 "AmazonEC2FullAccess".into(),
565 "AmazonRDSFullAccess".into(),
566 ],
567 },
568 IamUser {
570 username: "monitoring-svc".into(),
571 user_id: "AIDAI23XYZABCSTUVWX".into(),
572 arn: "arn:aws:iam::123456789012:user/monitoring-svc".into(),
573 created_at: "2024-03-01T08:00:00Z".into(),
574 permissions: vec!["CloudWatchReadOnlyAccess".into()],
575 },
576 IamUser {
578 username: "data-analyst".into(),
579 user_id: "AIDAI23XYZABCYZABCD".into(),
580 arn: "arn:aws:iam::123456789012:user/data-analyst".into(),
581 created_at: "2024-08-15T11:00:00Z".into(),
582 permissions: vec![
583 "AmazonS3ReadOnlyAccess".into(),
584 "AmazonAthenaFullAccess".into(),
585 ],
586 },
587 ]
588}
589
590fn seed_security_groups() -> Vec<SecurityGroup> {
591 vec![
592 SecurityGroup {
594 group_id: "sg-0a1b2c3d4e5f60001".into(),
595 group_name: "web-server-sg".into(),
596 description: "Security group for web servers".into(),
597 vpc_id: "vpc-abc123".into(),
598 inbound_rules: vec![
599 SecurityRule {
600 protocol: "tcp".into(),
601 port_range: "80".into(),
602 source: "0.0.0.0/0".into(),
603 },
604 SecurityRule {
605 protocol: "tcp".into(),
606 port_range: "443".into(),
607 source: "0.0.0.0/0".into(),
608 },
609 ],
610 outbound_rules: vec![SecurityRule {
611 protocol: "-1".into(),
612 port_range: "all".into(),
613 source: "0.0.0.0/0".into(),
614 }],
615 },
616 SecurityGroup {
618 group_id: "sg-0a1b2c3d4e5f60002".into(),
619 group_name: "database-sg".into(),
620 description: "Security group for databases".into(),
621 vpc_id: "vpc-abc123".into(),
622 inbound_rules: vec![
623 SecurityRule {
624 protocol: "tcp".into(),
625 port_range: "5432".into(),
626 source: "0.0.0.0/0".into(),
627 },
628 SecurityRule {
629 protocol: "tcp".into(),
630 port_range: "3306".into(),
631 source: "0.0.0.0/0".into(),
632 },
633 ],
634 outbound_rules: vec![SecurityRule {
635 protocol: "-1".into(),
636 port_range: "all".into(),
637 source: "0.0.0.0/0".into(),
638 }],
639 },
640 SecurityGroup {
642 group_id: "sg-0a1b2c3d4e5f60003".into(),
643 group_name: "admin-access-sg".into(),
644 description: "Security group for admin access".into(),
645 vpc_id: "vpc-abc123".into(),
646 inbound_rules: vec![
647 SecurityRule {
648 protocol: "tcp".into(),
649 port_range: "22".into(),
650 source: "0.0.0.0/0".into(),
651 },
652 SecurityRule {
653 protocol: "tcp".into(),
654 port_range: "3389".into(),
655 source: "0.0.0.0/0".into(),
656 },
657 ],
658 outbound_rules: vec![SecurityRule {
659 protocol: "-1".into(),
660 port_range: "all".into(),
661 source: "0.0.0.0/0".into(),
662 }],
663 },
664 ]
665}
666
667fn metric_profile_base(resource_id: &str, metric: &str) -> Option<f64> {
674 match (resource_id, metric) {
675 ("i-0a1b2c3d4e5f60001", "CPUUtilization") => Some(62.0),
677 ("i-0a1b2c3d4e5f60001", "MemoryUtilization") => Some(71.0),
678 ("i-0a1b2c3d4e5f60001", "NetworkIn") => Some(85_000.0),
679
680 ("i-0a1b2c3d4e5f60002", "CPUUtilization") => Some(48.0),
682 ("i-0a1b2c3d4e5f60002", "MemoryUtilization") => Some(55.0),
683 ("i-0a1b2c3d4e5f60002", "NetworkIn") => Some(72_000.0),
684
685 ("i-0a1b2c3d4e5f60003", "CPUUtilization") => Some(45.0),
687 ("i-0a1b2c3d4e5f60003", "MemoryUtilization") => Some(60.0),
688 ("i-0a1b2c3d4e5f60003", "NetworkIn") => Some(95_000.0),
689
690 ("i-0a1b2c3d4e5f60004", "CPUUtilization") => Some(42.0),
692 ("i-0a1b2c3d4e5f60004", "MemoryUtilization") => Some(58.0),
693 ("i-0a1b2c3d4e5f60004", "NetworkIn") => Some(88_000.0),
694
695 ("i-0a1b2c3d4e5f60005", "CPUUtilization") => Some(2.1),
697 ("i-0a1b2c3d4e5f60005", "MemoryUtilization") => Some(5.3),
698 ("i-0a1b2c3d4e5f60005", "NetworkIn") => Some(320.0),
699
700 ("i-0a1b2c3d4e5f60006", "CPUUtilization") => Some(4.8),
702 ("i-0a1b2c3d4e5f60006", "MemoryUtilization") => Some(12.0),
703 ("i-0a1b2c3d4e5f60006", "NetworkIn") => Some(1_200.0),
704
705 ("i-0a1b2c3d4e5f60007", "CPUUtilization") => Some(3.2),
707 ("i-0a1b2c3d4e5f60007", "MemoryUtilization") => Some(9.0),
708 ("i-0a1b2c3d4e5f60007", "NetworkIn") => Some(800.0),
709
710 ("prod-postgres-01", "CPUUtilization") => Some(38.0),
712 ("prod-postgres-01", "MemoryUtilization") => Some(65.0),
713
714 ("staging-mysql-01", "CPUUtilization") => Some(8.5),
716 ("staging-mysql-01", "MemoryUtilization") => Some(15.0),
717
718 ("legacy-pg-11", "CPUUtilization") => Some(6.0),
720 ("legacy-pg-11", "MemoryUtilization") => Some(22.0),
721
722 _ => None,
723 }
724}
725
726pub const FAKE_AWS_CAPABILITY_ID: &str = "fake_aws";
731
732pub struct FakeAwsCapability;
733
734impl Capability for FakeAwsCapability {
735 fn id(&self) -> &str {
736 FAKE_AWS_CAPABILITY_ID
737 }
738
739 fn name(&self) -> &str {
740 "Fake AWS Tools"
741 }
742
743 fn description(&self) -> &str {
744 "Mock AWS infrastructure tools (EC2, RDS, S3, IAM, CloudWatch). \
745 All state persisted in session filesystem. Latency emulation via FAKE_AWS_LATENCY_MS."
746 }
747
748 fn localizations(&self) -> Vec<CapabilityLocalization> {
749 vec![CapabilityLocalization::text(
750 "uk",
751 "Демо-інструменти AWS",
752 "Імітаційні інструменти інфраструктури AWS (EC2, RDS, S3, IAM, CloudWatch). Увесь стан зберігається у файловій системі сесії. Емуляція затримки через FAKE_AWS_LATENCY_MS.",
753 )]
754 }
755
756 fn status(&self) -> CapabilityStatus {
757 CapabilityStatus::Available
758 }
759
760 fn icon(&self) -> Option<&str> {
761 Some("cloud")
762 }
763
764 fn category(&self) -> Option<&str> {
765 Some("Demo Tools")
766 }
767
768 fn system_prompt_addition(&self) -> Option<&str> {
769 Some(
770 "AWS data is stored in /aws/ (ec2_instances.json, rds_databases.json, s3_buckets.json, iam_users.json, security_groups.json). API calls have realistic latency.",
771 )
772 }
773
774 fn tools(&self) -> Vec<Box<dyn Tool>> {
775 vec![
776 Box::new(AwsListEc2InstancesTool),
777 Box::new(AwsCreateEc2InstanceTool),
778 Box::new(AwsStopEc2InstanceTool),
779 Box::new(AwsListRdsDatabasesTool),
780 Box::new(AwsCreateRdsDatabaseTool),
781 Box::new(AwsListS3BucketsTool),
782 Box::new(AwsCreateS3BucketTool),
783 Box::new(AwsListIamUsersTool),
784 Box::new(AwsCreateIamUserTool),
785 Box::new(AwsListSecurityGroupsTool),
786 Box::new(AwsGetCloudWatchMetricsTool),
787 ]
788 }
789}
790
791pub struct AwsListEc2InstancesTool;
796
797#[async_trait]
798impl Tool for AwsListEc2InstancesTool {
799 fn name(&self) -> &str {
800 "aws_list_ec2_instances"
801 }
802
803 fn display_name(&self) -> Option<&str> {
804 Some("List EC2 Instances")
805 }
806
807 fn description(&self) -> &str {
808 "List all EC2 instances with their current status, IPs, and configuration."
809 }
810
811 fn parameters_schema(&self) -> Value {
812 json!({
813 "type": "object",
814 "properties": {
815 "state": {
816 "type": "string",
817 "enum": ["running", "stopped", "terminated"],
818 "description": "Optional: Filter by instance state"
819 }
820 },
821 "additionalProperties": false
822 })
823 }
824
825 fn hints(&self) -> ToolHints {
826 ToolHints::default()
827 .with_readonly(true)
828 .with_idempotent(true)
829 }
830
831 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
832 ToolExecutionResult::tool_error("aws_list_ec2_instances requires context")
833 }
834
835 async fn execute_with_context(
836 &self,
837 arguments: Value,
838 context: &ToolContext,
839 ) -> ToolExecutionResult {
840 let store = match require_file_store(context) {
841 Ok(store) => store,
842 Err(e) => return e,
843 };
844 simulate_latency(OpKind::List).await;
845
846 let state_filter = arguments.get("state").and_then(|v| v.as_str());
847 let instances = read_or_seed(store, context.session_id, EC2_PATH, seed_ec2).await;
848
849 let filtered: Vec<_> = if let Some(state) = state_filter {
850 instances.into_iter().filter(|i| i.state == state).collect()
851 } else {
852 instances
853 };
854
855 ToolExecutionResult::success(json!({
856 "instances": filtered,
857 "total_count": filtered.len()
858 }))
859 }
860
861 fn requires_context(&self) -> bool {
862 true
863 }
864}
865
866pub struct AwsCreateEc2InstanceTool;
871
872#[async_trait]
873impl Tool for AwsCreateEc2InstanceTool {
874 fn name(&self) -> &str {
875 "aws_create_ec2_instance"
876 }
877
878 fn display_name(&self) -> Option<&str> {
879 Some("Create EC2 Instance")
880 }
881
882 fn description(&self) -> &str {
883 "Launch a new EC2 instance with specified configuration."
884 }
885
886 fn parameters_schema(&self) -> Value {
887 json!({
888 "type": "object",
889 "properties": {
890 "instance_type": {
891 "type": "string",
892 "description": "Instance type (e.g., 't3.micro', 't3.medium')"
893 },
894 "name": {
895 "type": "string",
896 "description": "Instance name tag"
897 },
898 "availability_zone": {
899 "type": "string",
900 "description": "Availability zone (default: us-east-1a)"
901 }
902 },
903 "required": ["instance_type", "name"],
904 "additionalProperties": false
905 })
906 }
907
908 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
909 ToolExecutionResult::tool_error("aws_create_ec2_instance requires context")
910 }
911
912 async fn execute_with_context(
913 &self,
914 arguments: Value,
915 context: &ToolContext,
916 ) -> ToolExecutionResult {
917 let store = match require_file_store(context) {
918 Ok(store) => store,
919 Err(e) => return e,
920 };
921 simulate_latency(OpKind::Create).await;
922
923 let instance_type = match arguments.get("instance_type").and_then(|v| v.as_str()) {
924 Some(t) => t,
925 None => {
926 return ToolExecutionResult::tool_error(
927 "Missing required parameter: instance_type",
928 );
929 }
930 };
931 let name = match arguments.get("name").and_then(|v| v.as_str()) {
932 Some(n) => n,
933 None => return ToolExecutionResult::tool_error("Missing required parameter: name"),
934 };
935 let az = arguments
936 .get("availability_zone")
937 .and_then(|v| v.as_str())
938 .unwrap_or("us-east-1a");
939
940 let mut instances = read_or_seed(store, context.session_id, EC2_PATH, seed_ec2).await;
941
942 let instance_id = format!(
943 "i-{:016x}",
944 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64
945 );
946 let private_ip = format!("10.0.{}.{}", (instances.len() % 254) + 1, 100);
947 let public_ip = format!(
948 "54.{}.{}.{}",
949 100 + instances.len() % 100,
950 (instances.len() % 254) + 1,
951 100
952 );
953
954 let instance = Ec2Instance {
955 instance_id: instance_id.clone(),
956 instance_type: instance_type.to_string(),
957 state: "running".into(),
958 availability_zone: az.to_string(),
959 private_ip: private_ip.clone(),
960 public_ip: Some(public_ip.clone()),
961 launch_time: chrono::Utc::now().to_rfc3339(),
962 tags: vec![Tag {
963 key: "Name".into(),
964 value: name.to_string(),
965 }],
966 };
967 instances.push(instance);
968
969 match persist(store, context.session_id, EC2_PATH, &instances).await {
970 Ok(_) => ToolExecutionResult::success(json!({
971 "instance_id": instance_id,
972 "state": "running",
973 "private_ip": private_ip,
974 "public_ip": public_ip,
975 "message": "EC2 instance launched successfully"
976 })),
977 Err(e) => ToolExecutionResult::tool_error(format!("Failed to persist: {e}")),
978 }
979 }
980
981 fn requires_context(&self) -> bool {
982 true
983 }
984}
985
986pub struct AwsStopEc2InstanceTool;
991
992#[async_trait]
993impl Tool for AwsStopEc2InstanceTool {
994 fn name(&self) -> &str {
995 "aws_stop_ec2_instance"
996 }
997
998 fn display_name(&self) -> Option<&str> {
999 Some("Stop EC2 Instance")
1000 }
1001
1002 fn description(&self) -> &str {
1003 "Stop a running EC2 instance."
1004 }
1005
1006 fn parameters_schema(&self) -> Value {
1007 json!({
1008 "type": "object",
1009 "properties": {
1010 "instance_id": {
1011 "type": "string",
1012 "description": "Instance ID to stop"
1013 }
1014 },
1015 "required": ["instance_id"],
1016 "additionalProperties": false
1017 })
1018 }
1019
1020 fn hints(&self) -> ToolHints {
1021 ToolHints::default().with_destructive(true)
1022 }
1023
1024 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1025 ToolExecutionResult::tool_error("aws_stop_ec2_instance requires context")
1026 }
1027
1028 async fn execute_with_context(
1029 &self,
1030 arguments: Value,
1031 context: &ToolContext,
1032 ) -> ToolExecutionResult {
1033 let store = match require_file_store(context) {
1034 Ok(store) => store,
1035 Err(e) => return e,
1036 };
1037 simulate_latency(OpKind::Modify).await;
1038
1039 let instance_id = match arguments.get("instance_id").and_then(|v| v.as_str()) {
1040 Some(id) => id,
1041 None => {
1042 return ToolExecutionResult::tool_error("Missing required parameter: instance_id");
1043 }
1044 };
1045
1046 let mut instances = read_or_seed(store, context.session_id, EC2_PATH, seed_ec2).await;
1047
1048 let instance = match instances.iter_mut().find(|i| i.instance_id == instance_id) {
1049 Some(i) => i,
1050 None => {
1051 return ToolExecutionResult::tool_error(format!(
1052 "Instance not found: {}",
1053 instance_id
1054 ));
1055 }
1056 };
1057
1058 let old_state = instance.state.clone();
1059 instance.state = "stopped".into();
1060
1061 match persist(store, context.session_id, EC2_PATH, &instances).await {
1062 Ok(_) => ToolExecutionResult::success(json!({
1063 "instance_id": instance_id,
1064 "old_state": old_state,
1065 "new_state": "stopped"
1066 })),
1067 Err(e) => ToolExecutionResult::tool_error(format!("Failed to persist: {e}")),
1068 }
1069 }
1070
1071 fn requires_context(&self) -> bool {
1072 true
1073 }
1074}
1075
1076pub struct AwsListRdsDatabasesTool;
1081
1082#[async_trait]
1083impl Tool for AwsListRdsDatabasesTool {
1084 fn name(&self) -> &str {
1085 "aws_list_rds_databases"
1086 }
1087
1088 fn display_name(&self) -> Option<&str> {
1089 Some("List RDS Databases")
1090 }
1091
1092 fn description(&self) -> &str {
1093 "List all RDS database instances."
1094 }
1095
1096 fn parameters_schema(&self) -> Value {
1097 json!({"type": "object", "properties": {}, "additionalProperties": false})
1098 }
1099
1100 fn hints(&self) -> ToolHints {
1101 ToolHints::default()
1102 .with_readonly(true)
1103 .with_idempotent(true)
1104 }
1105
1106 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1107 ToolExecutionResult::tool_error("Requires context")
1108 }
1109
1110 async fn execute_with_context(
1111 &self,
1112 _arguments: Value,
1113 context: &ToolContext,
1114 ) -> ToolExecutionResult {
1115 let store = match require_file_store(context) {
1116 Ok(store) => store,
1117 Err(e) => return e,
1118 };
1119 simulate_latency(OpKind::List).await;
1120
1121 let databases = read_or_seed(store, context.session_id, RDS_PATH, seed_rds).await;
1122
1123 ToolExecutionResult::success(
1124 json!({"databases": databases, "total_count": databases.len()}),
1125 )
1126 }
1127
1128 fn requires_context(&self) -> bool {
1129 true
1130 }
1131}
1132
1133pub struct AwsCreateRdsDatabaseTool;
1138
1139#[async_trait]
1140impl Tool for AwsCreateRdsDatabaseTool {
1141 fn name(&self) -> &str {
1142 "aws_create_rds_database"
1143 }
1144
1145 fn display_name(&self) -> Option<&str> {
1146 Some("Create RDS Database")
1147 }
1148
1149 fn description(&self) -> &str {
1150 "Create a new RDS database instance."
1151 }
1152
1153 fn parameters_schema(&self) -> Value {
1154 json!({
1155 "type": "object",
1156 "properties": {
1157 "db_instance_id": {"type": "string"},
1158 "engine": {"type": "string", "enum": ["postgres", "mysql", "mariadb"]},
1159 "instance_class": {"type": "string"},
1160 "storage_gb": {"type": "integer"}
1161 },
1162 "required": ["db_instance_id", "engine", "instance_class"],
1163 "additionalProperties": false
1164 })
1165 }
1166
1167 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1168 ToolExecutionResult::tool_error("Requires context")
1169 }
1170
1171 async fn execute_with_context(
1172 &self,
1173 arguments: Value,
1174 context: &ToolContext,
1175 ) -> ToolExecutionResult {
1176 let store = match require_file_store(context) {
1177 Ok(store) => store,
1178 Err(e) => return e,
1179 };
1180 simulate_latency(OpKind::Create).await;
1181
1182 let db_id = match arguments.get("db_instance_id").and_then(|v| v.as_str()) {
1183 Some(id) => id,
1184 None => {
1185 return ToolExecutionResult::tool_error(
1186 "Missing required parameter: db_instance_id",
1187 );
1188 }
1189 };
1190 let engine = match arguments.get("engine").and_then(|v| v.as_str()) {
1191 Some(e) => e,
1192 None => return ToolExecutionResult::tool_error("Missing required parameter: engine"),
1193 };
1194 let instance_class = match arguments.get("instance_class").and_then(|v| v.as_str()) {
1195 Some(c) => c,
1196 None => {
1197 return ToolExecutionResult::tool_error(
1198 "Missing required parameter: instance_class",
1199 );
1200 }
1201 };
1202 let storage_gb = arguments
1203 .get("storage_gb")
1204 .and_then(|v| v.as_i64())
1205 .unwrap_or(20) as i32;
1206
1207 let engine_version = match engine {
1208 "postgres" => "15.4",
1209 "mysql" => "8.0",
1210 "mariadb" => "10.11",
1211 _ => "unknown",
1212 };
1213 let port = match engine {
1214 "postgres" => 5432,
1215 "mysql" | "mariadb" => 3306,
1216 _ => 5432,
1217 };
1218
1219 let mut databases = read_or_seed(store, context.session_id, RDS_PATH, seed_rds).await;
1220
1221 let db = RdsDatabase {
1222 db_instance_id: db_id.to_string(),
1223 engine: engine.to_string(),
1224 engine_version: engine_version.to_string(),
1225 instance_class: instance_class.to_string(),
1226 status: "creating".into(),
1227 endpoint: format!("{}.abc123.us-east-1.rds.amazonaws.com", db_id),
1228 port,
1229 storage_gb,
1230 };
1231 databases.push(db);
1232
1233 match persist(store, context.session_id, RDS_PATH, &databases).await {
1234 Ok(_) => ToolExecutionResult::success(json!({
1235 "db_instance_id": db_id,
1236 "status": "creating",
1237 "endpoint": format!("{}.abc123.us-east-1.rds.amazonaws.com", db_id),
1238 "message": "RDS database creation initiated"
1239 })),
1240 Err(e) => ToolExecutionResult::tool_error(format!("Failed to persist: {e}")),
1241 }
1242 }
1243
1244 fn requires_context(&self) -> bool {
1245 true
1246 }
1247}
1248
1249pub struct AwsListS3BucketsTool;
1254
1255#[async_trait]
1256impl Tool for AwsListS3BucketsTool {
1257 fn name(&self) -> &str {
1258 "aws_list_s3_buckets"
1259 }
1260
1261 fn display_name(&self) -> Option<&str> {
1262 Some("List S3 Buckets")
1263 }
1264
1265 fn description(&self) -> &str {
1266 "List all S3 buckets in the account."
1267 }
1268
1269 fn parameters_schema(&self) -> Value {
1270 json!({"type": "object", "properties": {}, "additionalProperties": false})
1271 }
1272
1273 fn hints(&self) -> ToolHints {
1274 ToolHints::default()
1275 .with_readonly(true)
1276 .with_idempotent(true)
1277 }
1278
1279 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1280 ToolExecutionResult::tool_error("Requires context")
1281 }
1282
1283 async fn execute_with_context(
1284 &self,
1285 _arguments: Value,
1286 context: &ToolContext,
1287 ) -> ToolExecutionResult {
1288 let store = match require_file_store(context) {
1289 Ok(store) => store,
1290 Err(e) => return e,
1291 };
1292 simulate_latency(OpKind::List).await;
1293
1294 let buckets = read_or_seed(store, context.session_id, S3_PATH, seed_s3).await;
1295
1296 ToolExecutionResult::success(json!({"buckets": buckets, "total_count": buckets.len()}))
1297 }
1298
1299 fn requires_context(&self) -> bool {
1300 true
1301 }
1302}
1303
1304pub struct AwsCreateS3BucketTool;
1309
1310#[async_trait]
1311impl Tool for AwsCreateS3BucketTool {
1312 fn name(&self) -> &str {
1313 "aws_create_s3_bucket"
1314 }
1315
1316 fn display_name(&self) -> Option<&str> {
1317 Some("Create S3 Bucket")
1318 }
1319
1320 fn description(&self) -> &str {
1321 "Create a new S3 bucket."
1322 }
1323
1324 fn parameters_schema(&self) -> Value {
1325 json!({
1326 "type": "object",
1327 "properties": {
1328 "bucket_name": {"type": "string"},
1329 "region": {"type": "string"},
1330 "versioning_enabled": {"type": "boolean"},
1331 "encryption_enabled": {"type": "boolean"}
1332 },
1333 "required": ["bucket_name"],
1334 "additionalProperties": false
1335 })
1336 }
1337
1338 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1339 ToolExecutionResult::tool_error("Requires context")
1340 }
1341
1342 async fn execute_with_context(
1343 &self,
1344 arguments: Value,
1345 context: &ToolContext,
1346 ) -> ToolExecutionResult {
1347 let store = match require_file_store(context) {
1348 Ok(store) => store,
1349 Err(e) => return e,
1350 };
1351 simulate_latency(OpKind::Create).await;
1352
1353 let bucket_name = match arguments.get("bucket_name").and_then(|v| v.as_str()) {
1354 Some(n) => n,
1355 None => {
1356 return ToolExecutionResult::tool_error("Missing required parameter: bucket_name");
1357 }
1358 };
1359 let region = arguments
1360 .get("region")
1361 .and_then(|v| v.as_str())
1362 .unwrap_or("us-east-1");
1363 let versioning = arguments
1364 .get("versioning_enabled")
1365 .and_then(|v| v.as_bool())
1366 .unwrap_or(false);
1367 let encryption = arguments
1368 .get("encryption_enabled")
1369 .and_then(|v| v.as_bool())
1370 .unwrap_or(true);
1371
1372 let mut buckets = read_or_seed(store, context.session_id, S3_PATH, seed_s3).await;
1373
1374 let bucket = S3Bucket {
1375 name: bucket_name.to_string(),
1376 region: region.to_string(),
1377 creation_date: chrono::Utc::now().to_rfc3339(),
1378 versioning_enabled: versioning,
1379 encryption_enabled: encryption,
1380 };
1381 buckets.push(bucket);
1382
1383 match persist(store, context.session_id, S3_PATH, &buckets).await {
1384 Ok(_) => ToolExecutionResult::success(json!({
1385 "bucket_name": bucket_name,
1386 "status": "created",
1387 "region": region
1388 })),
1389 Err(e) => ToolExecutionResult::tool_error(format!("Failed to persist: {e}")),
1390 }
1391 }
1392
1393 fn requires_context(&self) -> bool {
1394 true
1395 }
1396}
1397
1398pub struct AwsListIamUsersTool;
1403
1404#[async_trait]
1405impl Tool for AwsListIamUsersTool {
1406 fn name(&self) -> &str {
1407 "aws_list_iam_users"
1408 }
1409
1410 fn display_name(&self) -> Option<&str> {
1411 Some("List IAM Users")
1412 }
1413
1414 fn description(&self) -> &str {
1415 "List all IAM users in the account."
1416 }
1417
1418 fn parameters_schema(&self) -> Value {
1419 json!({"type": "object", "properties": {}, "additionalProperties": false})
1420 }
1421
1422 fn hints(&self) -> ToolHints {
1423 ToolHints::default()
1424 .with_readonly(true)
1425 .with_idempotent(true)
1426 }
1427
1428 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1429 ToolExecutionResult::tool_error("Requires context")
1430 }
1431
1432 async fn execute_with_context(
1433 &self,
1434 _arguments: Value,
1435 context: &ToolContext,
1436 ) -> ToolExecutionResult {
1437 let store = match require_file_store(context) {
1438 Ok(store) => store,
1439 Err(e) => return e,
1440 };
1441 simulate_latency(OpKind::List).await;
1442
1443 let users = read_or_seed(store, context.session_id, IAM_PATH, seed_iam).await;
1444
1445 ToolExecutionResult::success(json!({"users": users, "total_count": users.len()}))
1446 }
1447
1448 fn requires_context(&self) -> bool {
1449 true
1450 }
1451}
1452
1453pub struct AwsCreateIamUserTool;
1458
1459#[async_trait]
1460impl Tool for AwsCreateIamUserTool {
1461 fn name(&self) -> &str {
1462 "aws_create_iam_user"
1463 }
1464
1465 fn display_name(&self) -> Option<&str> {
1466 Some("Create IAM User")
1467 }
1468
1469 fn description(&self) -> &str {
1470 "Create a new IAM user."
1471 }
1472
1473 fn parameters_schema(&self) -> Value {
1474 json!({
1475 "type": "object",
1476 "properties": {
1477 "username": {"type": "string"},
1478 "permissions": {
1479 "type": "array",
1480 "items": {"type": "string"},
1481 "description": "List of permission policies"
1482 }
1483 },
1484 "required": ["username"],
1485 "additionalProperties": false
1486 })
1487 }
1488
1489 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1490 ToolExecutionResult::tool_error("Requires context")
1491 }
1492
1493 async fn execute_with_context(
1494 &self,
1495 arguments: Value,
1496 context: &ToolContext,
1497 ) -> ToolExecutionResult {
1498 let store = match require_file_store(context) {
1499 Ok(store) => store,
1500 Err(e) => return e,
1501 };
1502 simulate_latency(OpKind::Create).await;
1503
1504 let username = match arguments.get("username").and_then(|v| v.as_str()) {
1505 Some(u) => u,
1506 None => {
1507 return ToolExecutionResult::tool_error("Missing required parameter: username");
1508 }
1509 };
1510 let permissions: Vec<String> = arguments
1511 .get("permissions")
1512 .and_then(|v| v.as_array())
1513 .map(|arr| {
1514 arr.iter()
1515 .filter_map(|v| v.as_str().map(String::from))
1516 .collect()
1517 })
1518 .unwrap_or_default();
1519
1520 let mut users = read_or_seed(store, context.session_id, IAM_PATH, seed_iam).await;
1521
1522 let user_id = format!(
1523 "AIDAI{:016x}",
1524 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64
1525 );
1526 let user = IamUser {
1527 username: username.to_string(),
1528 user_id: user_id.clone(),
1529 arn: format!("arn:aws:iam::123456789012:user/{}", username),
1530 created_at: chrono::Utc::now().to_rfc3339(),
1531 permissions,
1532 };
1533 users.push(user);
1534
1535 match persist(store, context.session_id, IAM_PATH, &users).await {
1536 Ok(_) => ToolExecutionResult::success(json!({
1537 "username": username,
1538 "user_id": user_id,
1539 "status": "created"
1540 })),
1541 Err(e) => ToolExecutionResult::tool_error(format!("Failed to persist: {e}")),
1542 }
1543 }
1544
1545 fn requires_context(&self) -> bool {
1546 true
1547 }
1548}
1549
1550pub struct AwsListSecurityGroupsTool;
1555
1556#[async_trait]
1557impl Tool for AwsListSecurityGroupsTool {
1558 fn name(&self) -> &str {
1559 "aws_list_security_groups"
1560 }
1561
1562 fn display_name(&self) -> Option<&str> {
1563 Some("List Security Groups")
1564 }
1565
1566 fn description(&self) -> &str {
1567 "List all security groups with their rules."
1568 }
1569
1570 fn parameters_schema(&self) -> Value {
1571 json!({"type": "object", "properties": {}, "additionalProperties": false})
1572 }
1573
1574 fn hints(&self) -> ToolHints {
1575 ToolHints::default()
1576 .with_readonly(true)
1577 .with_idempotent(true)
1578 }
1579
1580 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1581 ToolExecutionResult::tool_error("Requires context")
1582 }
1583
1584 async fn execute_with_context(
1585 &self,
1586 _arguments: Value,
1587 context: &ToolContext,
1588 ) -> ToolExecutionResult {
1589 let store = match require_file_store(context) {
1590 Ok(store) => store,
1591 Err(e) => return e,
1592 };
1593 simulate_latency(OpKind::List).await;
1594
1595 let groups = read_or_seed(store, context.session_id, SG_PATH, seed_security_groups).await;
1596
1597 ToolExecutionResult::success(
1598 json!({"security_groups": groups, "total_count": groups.len()}),
1599 )
1600 }
1601
1602 fn requires_context(&self) -> bool {
1603 true
1604 }
1605}
1606
1607pub struct AwsGetCloudWatchMetricsTool;
1612
1613#[async_trait]
1614impl Tool for AwsGetCloudWatchMetricsTool {
1615 fn name(&self) -> &str {
1616 "aws_get_cloudwatch_metrics"
1617 }
1618
1619 fn display_name(&self) -> Option<&str> {
1620 Some("Get CloudWatch Metrics")
1621 }
1622
1623 fn description(&self) -> &str {
1624 "Get CloudWatch metrics for a resource (CPU, memory, disk, network)."
1625 }
1626
1627 fn parameters_schema(&self) -> Value {
1628 json!({
1629 "type": "object",
1630 "properties": {
1631 "resource_id": {"type": "string"},
1632 "metric_name": {
1633 "type": "string",
1634 "enum": ["CPUUtilization", "MemoryUtilization", "DiskReadOps", "NetworkIn"],
1635 "description": "Metric to retrieve"
1636 },
1637 "period_minutes": {
1638 "type": "integer",
1639 "description": "Time period in minutes (default: 60)"
1640 }
1641 },
1642 "required": ["resource_id", "metric_name"],
1643 "additionalProperties": false
1644 })
1645 }
1646
1647 fn hints(&self) -> ToolHints {
1648 ToolHints::default()
1649 .with_readonly(true)
1650 .with_idempotent(true)
1651 }
1652
1653 async fn execute(&self, _arguments: Value) -> ToolExecutionResult {
1654 ToolExecutionResult::tool_error("Requires context")
1655 }
1656
1657 async fn execute_with_context(
1658 &self,
1659 arguments: Value,
1660 context: &ToolContext,
1661 ) -> ToolExecutionResult {
1662 let store = match require_file_store(context) {
1663 Ok(store) => store,
1664 Err(e) => return e,
1665 };
1666 simulate_latency(OpKind::Query).await;
1667
1668 let resource_id = match arguments.get("resource_id").and_then(|v| v.as_str()) {
1669 Some(id) => id,
1670 None => {
1671 return ToolExecutionResult::tool_error("Missing required parameter: resource_id");
1672 }
1673 };
1674 let metric_name = match arguments.get("metric_name").and_then(|v| v.as_str()) {
1675 Some(m) => m,
1676 None => {
1677 return ToolExecutionResult::tool_error("Missing required parameter: metric_name");
1678 }
1679 };
1680
1681 let instances = read_or_seed(store, context.session_id, EC2_PATH, seed_ec2).await;
1683 let instance_exists = instances.iter().any(|i| i.instance_id == resource_id);
1684
1685 if !instance_exists {
1686 let databases = read_or_seed(store, context.session_id, RDS_PATH, seed_rds).await;
1688 let rds_exists = databases.iter().any(|d| d.db_instance_id == resource_id);
1689 if !rds_exists {
1690 return ToolExecutionResult::tool_error(format!(
1691 "Resource not found: {}",
1692 resource_id
1693 ));
1694 }
1695 }
1696
1697 let hash: u32 = resource_id
1701 .bytes()
1702 .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32));
1703 let base = metric_profile_base(resource_id, metric_name).unwrap_or(match metric_name {
1704 "CPUUtilization" => 30.0,
1705 "MemoryUtilization" => 55.0,
1706 "DiskReadOps" => 150.0,
1707 "NetworkIn" => 1024.0 * 50.0,
1708 _ => 30.0,
1709 });
1710 let datapoints: Vec<Value> = (0..12)
1711 .map(|i| {
1712 let variation = ((hash.wrapping_add(i * 7)) % 200) as f64 / 10.0 - 10.0;
1713 let avg = (base + variation * (base / 30.0)).max(0.0);
1714 json!({
1715 "timestamp": chrono::Utc::now() - chrono::Duration::minutes(i as i64 * 5),
1716 "average": (avg * 100.0).round() / 100.0,
1717 "minimum": ((avg * 0.7).max(0.0) * 100.0).round() / 100.0,
1718 "maximum": (avg * 1.3 * 100.0).round() / 100.0
1719 })
1720 })
1721 .collect();
1722
1723 ToolExecutionResult::success(json!({
1724 "resource_id": resource_id,
1725 "metric_name": metric_name,
1726 "datapoints": datapoints,
1727 "period": "5 minutes"
1728 }))
1729 }
1730
1731 fn requires_context(&self) -> bool {
1732 true
1733 }
1734}