diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index 3641dc8..6dbddcf 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -4,19 +4,16 @@ use std::sync::Arc; use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; use crank_core::{ - AgentId, AuditSink, AuthProfile, CapabilityProfile, CommunityCapabilityProfile, - EditionCapabilities, ExecutionMode, IdentityError, IdentityProvider, InvocationLevel, - InvocationLog, InvocationLogId, InvocationSource, InvocationStatus, MembershipRole, - NoopAuditSink, OperationId, OperationSecurityLevel, OperationStatus, OwnerOnlyPolicyEngine, - PlatformApiKeyScope, PlatformApiKeyStatus, PolicyEngine, ProductEdition, Protocol, + AuditSink, AuthProfile, CapabilityProfile, CommunityCapabilityProfile, EditionCapabilities, + ExecutionMode, IdentityError, IdentityProvider, InvocationLog, InvocationLogId, NoopAuditSink, + OperationSecurityLevel, OwnerOnlyPolicyEngine, PolicyEngine, ProductEdition, Protocol, ResponseCachePolicy, Target, ToolQualityMappingRule, ToolQualityMappingSet, - ToolQualitySchemaKind, ToolQualitySchemaNode, UsagePeriod, WizardState, WorkspaceId, + ToolQualitySchemaKind, ToolQualitySchemaNode, UsagePeriod, WorkspaceId, }; -use crank_mapping::{JsonPathRoot, MappingRule, MappingSet, infer_mapping_from_samples}; +use crank_mapping::{MappingRule, MappingSet}; use crank_registry::{ - AgentSummary, CreateInvocationLogRequest, ListInvocationLogsQuery, OperationAgentRef, - OperationSummary, OperationUsageSummary, PostgresRegistry, RegistryOperation, SampleKind, - UsageBucket, + AgentSummary, CreateInvocationLogRequest, OperationAgentRef, OperationSummary, + OperationUsageSummary, PostgresRegistry, RegistryOperation, UsageBucket, }; use crank_runtime::{PreparedRequest, ResolvedAuth, RuntimeError, RuntimeExecutor, SecretCrypto}; use crank_schema::{Schema, SchemaKind}; @@ -28,6 +25,7 @@ use uuid::Uuid; mod agents; mod api_keys; mod auth; +mod demo; mod import_export; mod observability; mod operations; @@ -379,282 +377,6 @@ impl AdminService { self.get_workspace(workspace_id).await.map(|_| ()) } - async fn seed_default_workspace_demo( - &self, - owner_user_id: &crank_core::UserId, - workspace_id: &WorkspaceId, - ) -> Result<(), ApiError> { - self.registry - .ensure_membership(workspace_id, owner_user_id, MembershipRole::Owner) - .await?; - - let rest_operation = self - .ensure_demo_operation(workspace_id, demo_rest_operation_payload()) - .await?; - self.ensure_operation_published(workspace_id, &rest_operation) - .await?; - self.ensure_demo_json_samples( - workspace_id, - &rest_operation.id, - &demo_rest_input_sample(), - &demo_rest_output_sample(), - ) - .await?; - - let archived_operation = self - .ensure_demo_operation(workspace_id, demo_archived_operation_payload()) - .await?; - self.ensure_operation_archived(workspace_id, &archived_operation) - .await?; - - let revops_agent = self - .ensure_demo_agent(workspace_id, demo_revops_agent_payload()) - .await?; - self.ensure_demo_agent_bindings( - workspace_id, - &AgentId::new(revops_agent.id.clone()), - vec![AgentBindingPayload { - operation_id: rest_operation.id.as_str().to_owned(), - operation_version: rest_operation.current_draft_version, - tool_name: "create_crm_lead".to_owned(), - tool_title: "Create CRM Lead".to_owned(), - tool_description_override: Some( - "Create a new CRM lead in the revenue workspace.".to_owned(), - ), - enabled: true, - }], - true, - ) - .await?; - - self.ensure_demo_platform_api_key( - workspace_id, - &AgentId::new(revops_agent.id.clone()), - "Web Console Demo Key", - vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], - false, - ) - .await?; - - self.seed_demo_invocation_logs( - workspace_id, - &AgentId::new(revops_agent.id), - &rest_operation.id, - ) - .await?; - - Ok(()) - } - - async fn ensure_demo_platform_api_key( - &self, - workspace_id: &WorkspaceId, - agent_id: &AgentId, - name: &str, - scopes: Vec, - revoke: bool, - ) -> Result<(), ApiError> { - let existing = self - .registry - .list_platform_api_keys(workspace_id) - .await? - .into_iter() - .find(|record| record.api_key.name == name); - let key = match existing { - Some(record) => record, - None => { - self.create_agent_platform_api_key( - workspace_id, - agent_id, - PlatformApiKeyPayload { - name: name.to_owned(), - scopes, - }, - ) - .await? - .api_key - } - }; - - if revoke && key.api_key.status != PlatformApiKeyStatus::Revoked { - self.revoke_agent_platform_api_key(workspace_id, agent_id, &key.api_key.id) - .await?; - } - - Ok(()) - } - - async fn ensure_demo_operation( - &self, - workspace_id: &WorkspaceId, - payload: OperationPayload, - ) -> Result { - if let Some(existing) = self - .find_operation_by_name(workspace_id, &payload.name) - .await? - { - return Ok(existing); - } - - let operation_name = payload.name.clone(); - self.create_operation(workspace_id, payload).await?; - self.find_operation_by_name(workspace_id, &operation_name) - .await? - .ok_or_else(|| ApiError::internal("demo operation was created but not found")) - } - - async fn ensure_operation_published( - &self, - workspace_id: &WorkspaceId, - summary: &OperationSummary, - ) -> Result<(), ApiError> { - if summary.latest_published_version.is_some() { - return Ok(()); - } - - self.publish_operation(workspace_id, &summary.id, summary.current_draft_version) - .await?; - Ok(()) - } - - async fn ensure_operation_archived( - &self, - workspace_id: &WorkspaceId, - summary: &OperationSummary, - ) -> Result<(), ApiError> { - if summary.status == OperationStatus::Archived { - return Ok(()); - } - - self.archive_operation(workspace_id, &summary.id).await?; - Ok(()) - } - - async fn ensure_demo_json_samples( - &self, - workspace_id: &WorkspaceId, - operation_id: &OperationId, - input: &Value, - output: &Value, - ) -> Result<(), ApiError> { - let summary = self.get_operation(workspace_id, operation_id).await?; - let samples = self - .registry - .list_sample_metadata(operation_id, summary.current_draft_version) - .await?; - - if !samples - .iter() - .any(|sample| sample.sample_kind == SampleKind::InputJson) - { - self.save_json_sample(workspace_id, operation_id, SampleKind::InputJson, input) - .await?; - } - - if !samples - .iter() - .any(|sample| sample.sample_kind == SampleKind::OutputJson) - { - self.save_json_sample(workspace_id, operation_id, SampleKind::OutputJson, output) - .await?; - } - - Ok(()) - } - - async fn ensure_demo_agent( - &self, - workspace_id: &WorkspaceId, - payload: AgentPayload, - ) -> Result { - let summary = - if let Some(existing) = self.find_agent_by_slug(workspace_id, &payload.slug).await? { - existing - } else { - self.create_agent(workspace_id, payload.clone()).await?; - self.find_agent_by_slug(workspace_id, &payload.slug) - .await? - .ok_or_else(|| ApiError::internal("demo agent was created but not found"))? - }; - - self.get_agent(workspace_id, &summary.id).await - } - - async fn ensure_demo_agent_bindings( - &self, - workspace_id: &WorkspaceId, - agent_id: &AgentId, - bindings: Vec, - publish: bool, - ) -> Result<(), ApiError> { - let summary = self.get_agent(workspace_id, agent_id).await?; - self.save_agent_bindings(workspace_id, agent_id, bindings) - .await?; - if publish && summary.latest_published_version.is_none() { - self.publish_agent(workspace_id, agent_id, summary.current_draft_version) - .await?; - } - - Ok(()) - } - - async fn seed_demo_invocation_logs( - &self, - workspace_id: &WorkspaceId, - revops_agent_id: &AgentId, - rest_operation_id: &OperationId, - ) -> Result<(), ApiError> { - if !self - .registry - .list_invocation_logs(ListInvocationLogsQuery { - workspace_id, - level: None, - search_text: None, - source: None, - operation_id: None, - agent_id: None, - created_after: None, - limit: 1, - }) - .await? - .is_empty() - { - return Ok(()); - } - - let rest_operation = self - .get_operation_version( - workspace_id, - rest_operation_id, - self.get_operation(workspace_id, rest_operation_id) - .await? - .current_draft_version, - ) - .await?; - self.record_invocation(InvocationRecordRequest { - workspace_id, - agent_id: Some(revops_agent_id), - operation: &rest_operation.snapshot, - request_id: None, - source: InvocationSource::AgentToolCall, - level: InvocationLevel::Info, - status: InvocationStatus::Ok, - message: "lead created in CRM".to_owned(), - status_code: Some(201), - error_kind: None, - duration_ms: 182, - request_preview: json!({ - "path": {}, - "query": {}, - "headers": { "x-demo-source": "crank-seed" }, - "body": demo_rest_request_sample() - }), - response_preview: demo_rest_response_sample(), - }) - .await?; - Ok(()) - } - async fn record_invocation( &self, request: InvocationRecordRequest<'_>, @@ -754,170 +476,6 @@ fn validate_profile_email(value: &str) -> Result { Ok(email) } -fn demo_revops_agent_payload() -> AgentPayload { - AgentPayload { - slug: "revops-copilot".to_owned(), - display_name: "RevOps Copilot".to_owned(), - description: "Sales operations assistant with CRM tools.".to_owned(), - instructions: json!({ - "system": "Prefer CRM mutations first, then lookup tools for confirmation." - }), - tool_selection_policy: json!({ - "max_tools": 8, - "prefer_tag": ["sales", "finance"] - }), - } -} - -fn demo_rest_operation_payload() -> OperationPayload { - let input = demo_rest_input_sample(); - let request = demo_rest_request_sample(); - let response = demo_rest_response_sample(); - let output = demo_rest_output_sample(); - - OperationPayload { - name: "crm_create_lead".to_owned(), - display_name: "Create CRM Lead".to_owned(), - category: "sales".to_owned(), - protocol: Protocol::Rest, - security_level: OperationSecurityLevel::Standard, - target: Target::Rest(crank_core::RestTarget { - base_url: "https://crm.demo.internal".to_owned(), - method: crank_core::HttpMethod::Post, - path_template: "/v1/leads".to_owned(), - static_headers: BTreeMap::from([("x-demo-source".to_owned(), "crank-seed".to_owned())]), - }), - input_schema: Schema::from_json_sample(&input), - output_schema: Schema::from_json_sample(&output), - input_mapping: infer_mapping_from_samples( - &input, - JsonPathRoot::Mcp, - &request, - JsonPathRoot::RequestBody, - ), - output_mapping: infer_mapping_from_samples( - &response, - JsonPathRoot::ResponseBody, - &output, - JsonPathRoot::Output, - ), - execution_config: crank_core::ExecutionConfig { - timeout_ms: 10_000, - retry_policy: None, - response_cache: None, - idempotency: None, - safety: None, - auth_profile_ref: None, - headers: BTreeMap::new(), - }, - tool_description: crank_core::ToolDescription { - title: "Create CRM Lead".to_owned(), - description: "Create a lead record in the CRM system.".to_owned(), - tags: vec!["sales".to_owned(), "crm".to_owned()], - examples: vec![crank_core::ToolExample { - input: json!({ - "email": "sarah.connor@example.com", - "company": "Cyberdyne" - }), - }], - }, - wizard_state: Some(WizardState { - input_sample: Some(input), - output_sample: Some(output), - test_input: Some(demo_rest_input_sample()), - }), - } -} - -fn demo_archived_operation_payload() -> OperationPayload { - let input = json!({ - "contactId": "contact_123", - "reason": "Duplicate profile" - }); - let request = json!({ - "contactId": "contact_123", - "reason": "Duplicate profile" - }); - let response = json!({ - "archived": true, - "contactId": "contact_123" - }); - - OperationPayload { - name: "marketing_archive_contact".to_owned(), - display_name: "Archive Marketing Contact".to_owned(), - category: "marketing".to_owned(), - protocol: Protocol::Rest, - security_level: OperationSecurityLevel::Standard, - target: Target::Rest(crank_core::RestTarget { - base_url: "https://marketing.demo.internal".to_owned(), - method: crank_core::HttpMethod::Patch, - path_template: "/v1/contacts/archive".to_owned(), - static_headers: BTreeMap::new(), - }), - input_schema: Schema::from_json_sample(&input), - output_schema: Schema::from_json_sample(&response), - input_mapping: infer_mapping_from_samples( - &input, - JsonPathRoot::Mcp, - &request, - JsonPathRoot::RequestBody, - ), - output_mapping: infer_mapping_from_samples( - &response, - JsonPathRoot::ResponseBody, - &response, - JsonPathRoot::Output, - ), - execution_config: crank_core::ExecutionConfig { - timeout_ms: 6_000, - retry_policy: None, - response_cache: None, - idempotency: None, - safety: None, - auth_profile_ref: None, - headers: BTreeMap::new(), - }, - tool_description: crank_core::ToolDescription { - title: "Archive Marketing Contact".to_owned(), - description: "Legacy archived flow kept for audit only.".to_owned(), - tags: vec!["marketing".to_owned()], - examples: Vec::new(), - }, - wizard_state: Some(WizardState { - input_sample: Some(input), - output_sample: Some(response), - test_input: Some(request), - }), - } -} - -fn demo_rest_input_sample() -> Value { - json!({ - "firstName": "Sarah", - "lastName": "Connor", - "email": "sarah.connor@example.com", - "company": "Cyberdyne", - "source": "website" - }) -} - -fn demo_rest_request_sample() -> Value { - demo_rest_input_sample() -} - -fn demo_rest_response_sample() -> Value { - json!({ - "id": "lead_1001", - "status": "created", - "owner": "revops" - }) -} - -fn demo_rest_output_sample() -> Value { - demo_rest_response_sample() -} - fn new_prefixed_id(prefix: &str) -> String { format!("{prefix}_{}", Uuid::now_v7().simple()) } diff --git a/apps/admin-api/src/service/demo.rs b/apps/admin-api/src/service/demo.rs new file mode 100644 index 0000000..e07487f --- /dev/null +++ b/apps/admin-api/src/service/demo.rs @@ -0,0 +1,461 @@ +use std::collections::BTreeMap; + +use crank_core::{ + AgentId, InvocationLevel, InvocationSource, InvocationStatus, MembershipRole, OperationId, + OperationSecurityLevel, OperationStatus, PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, + Target, WizardState, WorkspaceId, +}; +use crank_mapping::{JsonPathRoot, infer_mapping_from_samples}; +use crank_registry::{ListInvocationLogsQuery, OperationSummary, SampleKind}; +use crank_schema::Schema; +use serde_json::{Value, json}; + +use crate::{ + error::ApiError, + service::{ + AdminService, AgentBindingPayload, AgentPayload, AgentSummaryView, InvocationRecordRequest, + OperationPayload, PlatformApiKeyPayload, + }, +}; + +impl AdminService { + pub(super) async fn seed_default_workspace_demo( + &self, + owner_user_id: &crank_core::UserId, + workspace_id: &WorkspaceId, + ) -> Result<(), ApiError> { + self.registry + .ensure_membership(workspace_id, owner_user_id, MembershipRole::Owner) + .await?; + + let rest_operation = self + .ensure_demo_operation(workspace_id, demo_rest_operation_payload()) + .await?; + self.ensure_operation_published(workspace_id, &rest_operation) + .await?; + self.ensure_demo_json_samples( + workspace_id, + &rest_operation.id, + &demo_rest_input_sample(), + &demo_rest_output_sample(), + ) + .await?; + + let archived_operation = self + .ensure_demo_operation(workspace_id, demo_archived_operation_payload()) + .await?; + self.ensure_operation_archived(workspace_id, &archived_operation) + .await?; + + let revops_agent = self + .ensure_demo_agent(workspace_id, demo_revops_agent_payload()) + .await?; + self.ensure_demo_agent_bindings( + workspace_id, + &AgentId::new(revops_agent.id.clone()), + vec![AgentBindingPayload { + operation_id: rest_operation.id.as_str().to_owned(), + operation_version: rest_operation.current_draft_version, + tool_name: "create_crm_lead".to_owned(), + tool_title: "Create CRM Lead".to_owned(), + tool_description_override: Some( + "Create a new CRM lead in the revenue workspace.".to_owned(), + ), + enabled: true, + }], + true, + ) + .await?; + + self.ensure_demo_platform_api_key( + workspace_id, + &AgentId::new(revops_agent.id.clone()), + "Web Console Demo Key", + vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], + false, + ) + .await?; + + self.seed_demo_invocation_logs( + workspace_id, + &AgentId::new(revops_agent.id), + &rest_operation.id, + ) + .await?; + + Ok(()) + } + + async fn ensure_demo_platform_api_key( + &self, + workspace_id: &WorkspaceId, + agent_id: &AgentId, + name: &str, + scopes: Vec, + revoke: bool, + ) -> Result<(), ApiError> { + let existing = self + .registry + .list_platform_api_keys(workspace_id) + .await? + .into_iter() + .find(|record| record.api_key.name == name); + let key = match existing { + Some(record) => record, + None => { + self.create_agent_platform_api_key( + workspace_id, + agent_id, + PlatformApiKeyPayload { + name: name.to_owned(), + scopes, + }, + ) + .await? + .api_key + } + }; + + if revoke && key.api_key.status != PlatformApiKeyStatus::Revoked { + self.revoke_agent_platform_api_key(workspace_id, agent_id, &key.api_key.id) + .await?; + } + + Ok(()) + } + + async fn ensure_demo_operation( + &self, + workspace_id: &WorkspaceId, + payload: OperationPayload, + ) -> Result { + if let Some(existing) = self + .find_operation_by_name(workspace_id, &payload.name) + .await? + { + return Ok(existing); + } + + let operation_name = payload.name.clone(); + self.create_operation(workspace_id, payload).await?; + self.find_operation_by_name(workspace_id, &operation_name) + .await? + .ok_or_else(|| ApiError::internal("demo operation was created but not found")) + } + + async fn ensure_operation_published( + &self, + workspace_id: &WorkspaceId, + summary: &OperationSummary, + ) -> Result<(), ApiError> { + if summary.latest_published_version.is_some() { + return Ok(()); + } + + self.publish_operation(workspace_id, &summary.id, summary.current_draft_version) + .await?; + Ok(()) + } + + async fn ensure_operation_archived( + &self, + workspace_id: &WorkspaceId, + summary: &OperationSummary, + ) -> Result<(), ApiError> { + if summary.status == OperationStatus::Archived { + return Ok(()); + } + + self.archive_operation(workspace_id, &summary.id).await?; + Ok(()) + } + + async fn ensure_demo_json_samples( + &self, + workspace_id: &WorkspaceId, + operation_id: &OperationId, + input: &Value, + output: &Value, + ) -> Result<(), ApiError> { + let summary = self.get_operation(workspace_id, operation_id).await?; + let samples = self + .registry + .list_sample_metadata(operation_id, summary.current_draft_version) + .await?; + + if !samples + .iter() + .any(|sample| sample.sample_kind == SampleKind::InputJson) + { + self.save_json_sample(workspace_id, operation_id, SampleKind::InputJson, input) + .await?; + } + + if !samples + .iter() + .any(|sample| sample.sample_kind == SampleKind::OutputJson) + { + self.save_json_sample(workspace_id, operation_id, SampleKind::OutputJson, output) + .await?; + } + + Ok(()) + } + + async fn ensure_demo_agent( + &self, + workspace_id: &WorkspaceId, + payload: AgentPayload, + ) -> Result { + let summary = + if let Some(existing) = self.find_agent_by_slug(workspace_id, &payload.slug).await? { + existing + } else { + self.create_agent(workspace_id, payload.clone()).await?; + self.find_agent_by_slug(workspace_id, &payload.slug) + .await? + .ok_or_else(|| ApiError::internal("demo agent was created but not found"))? + }; + + self.get_agent(workspace_id, &summary.id).await + } + + async fn ensure_demo_agent_bindings( + &self, + workspace_id: &WorkspaceId, + agent_id: &AgentId, + bindings: Vec, + publish: bool, + ) -> Result<(), ApiError> { + let summary = self.get_agent(workspace_id, agent_id).await?; + self.save_agent_bindings(workspace_id, agent_id, bindings) + .await?; + if publish && summary.latest_published_version.is_none() { + self.publish_agent(workspace_id, agent_id, summary.current_draft_version) + .await?; + } + + Ok(()) + } + + async fn seed_demo_invocation_logs( + &self, + workspace_id: &WorkspaceId, + revops_agent_id: &AgentId, + rest_operation_id: &OperationId, + ) -> Result<(), ApiError> { + if !self + .registry + .list_invocation_logs(ListInvocationLogsQuery { + workspace_id, + level: None, + search_text: None, + source: None, + operation_id: None, + agent_id: None, + created_after: None, + limit: 1, + }) + .await? + .is_empty() + { + return Ok(()); + } + + let rest_operation = self + .get_operation_version( + workspace_id, + rest_operation_id, + self.get_operation(workspace_id, rest_operation_id) + .await? + .current_draft_version, + ) + .await?; + self.record_invocation(InvocationRecordRequest { + workspace_id, + agent_id: Some(revops_agent_id), + operation: &rest_operation.snapshot, + request_id: None, + source: InvocationSource::AgentToolCall, + level: InvocationLevel::Info, + status: InvocationStatus::Ok, + message: "lead created in CRM".to_owned(), + status_code: Some(201), + error_kind: None, + duration_ms: 182, + request_preview: json!({ + "path": {}, + "query": {}, + "headers": { "x-demo-source": "crank-seed" }, + "body": demo_rest_request_sample() + }), + response_preview: demo_rest_response_sample(), + }) + .await?; + Ok(()) + } +} + +fn demo_revops_agent_payload() -> AgentPayload { + AgentPayload { + slug: "revops-copilot".to_owned(), + display_name: "RevOps Copilot".to_owned(), + description: "Sales operations assistant with CRM tools.".to_owned(), + instructions: json!({ + "system": "Prefer CRM mutations first, then lookup tools for confirmation." + }), + tool_selection_policy: json!({ + "max_tools": 8, + "prefer_tag": ["sales", "finance"] + }), + } +} + +fn demo_rest_operation_payload() -> OperationPayload { + let input = demo_rest_input_sample(); + let request = demo_rest_request_sample(); + let response = demo_rest_response_sample(); + let output = demo_rest_output_sample(); + + OperationPayload { + name: "crm_create_lead".to_owned(), + display_name: "Create CRM Lead".to_owned(), + category: "sales".to_owned(), + protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, + target: Target::Rest(crank_core::RestTarget { + base_url: "https://crm.demo.internal".to_owned(), + method: crank_core::HttpMethod::Post, + path_template: "/v1/leads".to_owned(), + static_headers: BTreeMap::from([("x-demo-source".to_owned(), "crank-seed".to_owned())]), + }), + input_schema: Schema::from_json_sample(&input), + output_schema: Schema::from_json_sample(&output), + input_mapping: infer_mapping_from_samples( + &input, + JsonPathRoot::Mcp, + &request, + JsonPathRoot::RequestBody, + ), + output_mapping: infer_mapping_from_samples( + &response, + JsonPathRoot::ResponseBody, + &output, + JsonPathRoot::Output, + ), + execution_config: crank_core::ExecutionConfig { + timeout_ms: 10_000, + retry_policy: None, + response_cache: None, + idempotency: None, + safety: None, + auth_profile_ref: None, + headers: BTreeMap::new(), + }, + tool_description: crank_core::ToolDescription { + title: "Create CRM Lead".to_owned(), + description: "Create a lead record in the CRM system.".to_owned(), + tags: vec!["sales".to_owned(), "crm".to_owned()], + examples: vec![crank_core::ToolExample { + input: json!({ + "email": "sarah.connor@example.com", + "company": "Cyberdyne" + }), + }], + }, + wizard_state: Some(WizardState { + input_sample: Some(input), + output_sample: Some(output), + test_input: Some(demo_rest_input_sample()), + }), + } +} + +fn demo_archived_operation_payload() -> OperationPayload { + let input = json!({ + "contactId": "contact_123", + "reason": "Duplicate profile" + }); + let request = json!({ + "contactId": "contact_123", + "reason": "Duplicate profile" + }); + let response = json!({ + "archived": true, + "contactId": "contact_123" + }); + + OperationPayload { + name: "marketing_archive_contact".to_owned(), + display_name: "Archive Marketing Contact".to_owned(), + category: "marketing".to_owned(), + protocol: Protocol::Rest, + security_level: OperationSecurityLevel::Standard, + target: Target::Rest(crank_core::RestTarget { + base_url: "https://marketing.demo.internal".to_owned(), + method: crank_core::HttpMethod::Patch, + path_template: "/v1/contacts/archive".to_owned(), + static_headers: BTreeMap::new(), + }), + input_schema: Schema::from_json_sample(&input), + output_schema: Schema::from_json_sample(&response), + input_mapping: infer_mapping_from_samples( + &input, + JsonPathRoot::Mcp, + &request, + JsonPathRoot::RequestBody, + ), + output_mapping: infer_mapping_from_samples( + &response, + JsonPathRoot::ResponseBody, + &response, + JsonPathRoot::Output, + ), + execution_config: crank_core::ExecutionConfig { + timeout_ms: 6_000, + retry_policy: None, + response_cache: None, + idempotency: None, + safety: None, + auth_profile_ref: None, + headers: BTreeMap::new(), + }, + tool_description: crank_core::ToolDescription { + title: "Archive Marketing Contact".to_owned(), + description: "Legacy archived flow kept for audit only.".to_owned(), + tags: vec!["marketing".to_owned()], + examples: Vec::new(), + }, + wizard_state: Some(WizardState { + input_sample: Some(input), + output_sample: Some(response), + test_input: Some(request), + }), + } +} + +fn demo_rest_input_sample() -> Value { + json!({ + "firstName": "Sarah", + "lastName": "Connor", + "email": "sarah.connor@example.com", + "company": "Cyberdyne", + "source": "website" + }) +} + +fn demo_rest_request_sample() -> Value { + demo_rest_input_sample() +} + +fn demo_rest_response_sample() -> Value { + json!({ + "id": "lead_1001", + "status": "created", + "owner": "revops" + }) +} + +fn demo_rest_output_sample() -> Value { + demo_rest_response_sample() +}