Remove enterprise leftovers from community
This commit is contained in:
+1
-1450
File diff suppressed because it is too large
Load Diff
@@ -109,13 +109,6 @@ impl ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn forbidden_with_context(message: impl Into<String>, context: Value) -> Self {
|
||||
Self::Forbidden {
|
||||
message: message.into(),
|
||||
context: Some(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
Self::Unauthorized { .. } => StatusCode::UNAUTHORIZED,
|
||||
@@ -224,14 +217,6 @@ impl From<RegistryError> for ApiError {
|
||||
format!("secret {secret_id} was not found"),
|
||||
json!({ "secret_id": secret_id }),
|
||||
),
|
||||
RegistryError::StreamSessionNotFound { session_id } => Self::not_found_with_context(
|
||||
format!("stream session {session_id} was not found"),
|
||||
json!({ "session_id": session_id }),
|
||||
),
|
||||
RegistryError::AsyncJobNotFound { job_id } => Self::not_found_with_context(
|
||||
format!("async job {job_id} was not found"),
|
||||
json!({ "job_id": job_id }),
|
||||
),
|
||||
RegistryError::InvocationLogNotFound { log_id } => Self::not_found_with_context(
|
||||
format!("invocation log {log_id} was not found"),
|
||||
json!({ "log_id": log_id }),
|
||||
@@ -299,28 +284,6 @@ impl From<RegistryError> for ApiError {
|
||||
"auth_profile_id": auth_profile_id,
|
||||
}),
|
||||
),
|
||||
RegistryError::InvalidStreamSessionTransition {
|
||||
session_id,
|
||||
from,
|
||||
to,
|
||||
} => Self::conflict_with_context(
|
||||
format!("invalid stream session transition for {session_id}: {from} -> {to}"),
|
||||
json!({
|
||||
"session_id": session_id,
|
||||
"from": from,
|
||||
"to": to,
|
||||
}),
|
||||
),
|
||||
RegistryError::InvalidAsyncJobTransition { job_id, from, to } => {
|
||||
Self::conflict_with_context(
|
||||
format!("invalid async job transition for {job_id}: {from} -> {to}"),
|
||||
json!({
|
||||
"job_id": job_id,
|
||||
"from": from,
|
||||
"to": to,
|
||||
}),
|
||||
)
|
||||
}
|
||||
RegistryError::UserEmailAlreadyExists { email } => Self::conflict_with_context(
|
||||
format!("user with email {email} already exists"),
|
||||
json!({ "email": email }),
|
||||
@@ -490,8 +453,7 @@ pub fn runtime_error_context(error: &RuntimeError) -> Option<Value> {
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::{ApiError, runtime_error_context, runtime_test_failure};
|
||||
use crank_registry::RegistryError;
|
||||
use super::{runtime_error_context, runtime_test_failure};
|
||||
use crank_runtime::RuntimeError;
|
||||
|
||||
#[test]
|
||||
@@ -544,27 +506,4 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_errors_preserve_structured_context_in_api_error() {
|
||||
let error = ApiError::from(RegistryError::InvalidAsyncJobTransition {
|
||||
job_id: "job_123".to_owned(),
|
||||
from: "running".to_owned(),
|
||||
to: "completed".to_owned(),
|
||||
});
|
||||
|
||||
match error {
|
||||
ApiError::Conflict { context, .. } => {
|
||||
assert_eq!(
|
||||
context,
|
||||
Some(json!({
|
||||
"job_id": "job_123",
|
||||
"from": "running",
|
||||
"to": "completed"
|
||||
}))
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected error variant: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,9 @@ pub mod agents;
|
||||
pub mod auth;
|
||||
pub mod auth_profiles;
|
||||
pub mod capabilities;
|
||||
pub mod machine_auth;
|
||||
pub mod observability;
|
||||
pub mod operations;
|
||||
pub mod secrets;
|
||||
pub mod streaming;
|
||||
pub mod upstreams;
|
||||
pub mod workspaces;
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
use axum::{Json, extract::State};
|
||||
use crank_core::{
|
||||
IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, MachineAccessMode, MembershipRole,
|
||||
TokenIssuerActor, TokenIssuerError, UserId, WorkspaceId,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{error::ApiError, state::AppState};
|
||||
|
||||
pub async fn issue_agent_token(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<IssueAgentTokenRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let edition = state.service.capability_profile().capabilities().edition;
|
||||
let grant_type = payload.grant_type.clone();
|
||||
let response = state
|
||||
.service
|
||||
.token_issuer()
|
||||
.issue_short_lived(payload, &community_token_issuer_actor())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
map_token_issuer_error(
|
||||
error,
|
||||
edition,
|
||||
MachineAccessMode::ShortLivedToken,
|
||||
json!({
|
||||
"grant_type": grant_type,
|
||||
"upgrade_required": true,
|
||||
}),
|
||||
)
|
||||
})?;
|
||||
Ok(Json(serde_json::json!(response)))
|
||||
}
|
||||
|
||||
pub async fn issue_one_time_agent_token(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<IssueOneTimeAgentTokenRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let edition = state.service.capability_profile().capabilities().edition;
|
||||
let operation_id = payload.operation_id.as_str().to_owned();
|
||||
let response = state
|
||||
.service
|
||||
.token_issuer()
|
||||
.issue_one_time(payload, &community_token_issuer_actor())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
map_token_issuer_error(
|
||||
error,
|
||||
edition,
|
||||
MachineAccessMode::OneTimeToken,
|
||||
json!({
|
||||
"operation_id": operation_id,
|
||||
"upgrade_required": true,
|
||||
}),
|
||||
)
|
||||
})?;
|
||||
Ok(Json(serde_json::json!(response)))
|
||||
}
|
||||
|
||||
fn community_token_issuer_actor() -> TokenIssuerActor {
|
||||
TokenIssuerActor {
|
||||
user_id: UserId::new("user_community_public"),
|
||||
workspace_id: WorkspaceId::new("ws_community_public"),
|
||||
role: MembershipRole::Viewer,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_token_issuer_error(
|
||||
error: TokenIssuerError,
|
||||
edition: crank_core::ProductEdition,
|
||||
machine_access_mode: MachineAccessMode,
|
||||
extra_context: serde_json::Value,
|
||||
) -> ApiError {
|
||||
match error {
|
||||
TokenIssuerError::NotSupportedInEdition => ApiError::forbidden_with_context(
|
||||
match machine_access_mode {
|
||||
MachineAccessMode::ShortLivedToken => {
|
||||
"short-lived machine access is not available in Community"
|
||||
}
|
||||
MachineAccessMode::OneTimeToken => {
|
||||
"one-time machine access is not available in Community"
|
||||
}
|
||||
MachineAccessMode::StaticAgentKey => {
|
||||
"static agent key machine access is not available for token issue"
|
||||
}
|
||||
},
|
||||
merge_machine_auth_context(edition, machine_access_mode, extra_context),
|
||||
),
|
||||
TokenIssuerError::InvalidGrant(reason) => ApiError::validation_with_context(
|
||||
"invalid machine token grant",
|
||||
json!({ "reason": reason }),
|
||||
),
|
||||
TokenIssuerError::AgentKeyUnknown => ApiError::validation("agent key is unknown"),
|
||||
TokenIssuerError::OperationNotStrict => ApiError::validation("operation is not strict"),
|
||||
TokenIssuerError::OperationNotPublishedForAgent => {
|
||||
ApiError::validation("operation is not published for agent")
|
||||
}
|
||||
TokenIssuerError::RegistryFailure(details) => {
|
||||
ApiError::internal(format!("token issuer registry failure: {details}"))
|
||||
}
|
||||
TokenIssuerError::ReplayGuardFailure(details) => {
|
||||
ApiError::internal(format!("token issuer replay guard failure: {details}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_machine_auth_context(
|
||||
edition: crank_core::ProductEdition,
|
||||
machine_access_mode: MachineAccessMode,
|
||||
extra_context: serde_json::Value,
|
||||
) -> serde_json::Value {
|
||||
let mut context = json!({
|
||||
"edition": edition,
|
||||
"machine_access_mode": machine_access_mode,
|
||||
});
|
||||
|
||||
if let (Some(base), Some(extra)) = (context.as_object_mut(), extra_context.as_object()) {
|
||||
for (key, value) in extra {
|
||||
base.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
context
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{error::ApiError, routes::access::WorkspacePath, state::AppState};
|
||||
|
||||
pub async fn list_protocol_capabilities(
|
||||
Path(_path): Path<WorkspacePath>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
Ok(Json(json!({
|
||||
"items": state.service.list_protocol_capabilities().await
|
||||
})))
|
||||
}
|
||||
@@ -8,12 +8,12 @@ use crank_core::{
|
||||
AuthKind, AuthProfile, AuthProfileId, CapabilityProfile, CommunityCapabilityProfile,
|
||||
ConfigExport, EditionCapabilities, ExecutionMode, ExportMode, GeneratedDraft,
|
||||
GeneratedDraftStatus, IdentityError, IdentityProvider, InvocationLevel, InvocationLog,
|
||||
InvocationLogId, InvocationSource, InvocationStatus, LoginOutcome, MachineTokenIssuer,
|
||||
MembershipRole, NoMachineTokenIssuer, NoopAuditSink, OperationId, OperationSecurityLevel,
|
||||
OperationStatus, OwnerOnlyPolicyEngine, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope,
|
||||
PlatformApiKeyStatus, PolicyEngine, ProductEdition, Protocol, ResponseCachePolicy, SampleId,
|
||||
Samples, Secret, SecretId, SecretKind, SecretStatus, Target, UsagePeriod, UserId,
|
||||
UserSessionId, WizardState, Workspace, WorkspaceId, WorkspaceStatus,
|
||||
InvocationLogId, InvocationSource, InvocationStatus, LoginOutcome, MembershipRole,
|
||||
NoopAuditSink, OperationId, OperationSecurityLevel, OperationStatus, OwnerOnlyPolicyEngine,
|
||||
PlatformApiKey, PlatformApiKeyId, PlatformApiKeyScope, PlatformApiKeyStatus, PolicyEngine,
|
||||
ProductEdition, Protocol, ResponseCachePolicy, SampleId, Samples, Secret, SecretId, SecretKind,
|
||||
SecretStatus, Target, UsagePeriod, UserId, UserSessionId, WizardState, Workspace, WorkspaceId,
|
||||
WorkspaceStatus,
|
||||
};
|
||||
use crank_mapping::{JsonPathRoot, MappingSet, infer_mapping_from_samples};
|
||||
use crank_registry::{
|
||||
@@ -59,7 +59,6 @@ pub struct AdminService {
|
||||
identity_provider: Option<Arc<dyn IdentityProvider>>,
|
||||
policy_engine: Arc<dyn PolicyEngine>,
|
||||
audit_sink: Arc<dyn AuditSink>,
|
||||
token_issuer: Arc<dyn MachineTokenIssuer>,
|
||||
capability_profile: Arc<dyn CapabilityProfile>,
|
||||
}
|
||||
|
||||
@@ -72,7 +71,6 @@ pub struct AdminServiceBuilder {
|
||||
identity_provider: Option<Arc<dyn IdentityProvider>>,
|
||||
policy_engine: Option<Arc<dyn PolicyEngine>>,
|
||||
audit_sink: Option<Arc<dyn AuditSink>>,
|
||||
token_issuer: Option<Arc<dyn MachineTokenIssuer>>,
|
||||
capability_profile: Option<Arc<dyn CapabilityProfile>>,
|
||||
}
|
||||
|
||||
@@ -508,21 +506,6 @@ pub struct DescriptorUploadResponse {
|
||||
pub version: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct GrpcServiceSummary {
|
||||
pub package: String,
|
||||
pub service: String,
|
||||
pub methods: Vec<GrpcMethodSummary>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct GrpcMethodSummary {
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub input_schema: Schema,
|
||||
pub output_schema: Schema,
|
||||
}
|
||||
|
||||
fn default_operation_category() -> String {
|
||||
"general".to_owned()
|
||||
}
|
||||
@@ -574,10 +557,6 @@ impl AdminService {
|
||||
&self.audit_sink
|
||||
}
|
||||
|
||||
pub fn token_issuer(&self) -> &Arc<dyn MachineTokenIssuer> {
|
||||
&self.token_issuer
|
||||
}
|
||||
|
||||
pub fn capability_profile(&self) -> &Arc<dyn CapabilityProfile> {
|
||||
&self.capability_profile
|
||||
}
|
||||
@@ -600,7 +579,6 @@ impl AdminServiceBuilder {
|
||||
identity_provider: None,
|
||||
policy_engine: None,
|
||||
audit_sink: None,
|
||||
token_issuer: None,
|
||||
capability_profile: None,
|
||||
}
|
||||
}
|
||||
@@ -622,12 +600,6 @@ impl AdminServiceBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_token_issuer(mut self, token_issuer: Arc<dyn MachineTokenIssuer>) -> Self {
|
||||
self.token_issuer = Some(token_issuer);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_capability_profile(
|
||||
mut self,
|
||||
@@ -649,9 +621,6 @@ impl AdminServiceBuilder {
|
||||
.policy_engine
|
||||
.unwrap_or_else(|| Arc::new(OwnerOnlyPolicyEngine)),
|
||||
audit_sink: self.audit_sink.unwrap_or_else(|| Arc::new(NoopAuditSink)),
|
||||
token_issuer: self
|
||||
.token_issuer
|
||||
.unwrap_or_else(|| Arc::new(NoMachineTokenIssuer)),
|
||||
capability_profile: self
|
||||
.capability_profile
|
||||
.unwrap_or_else(|| Arc::new(CommunityCapabilityProfile)),
|
||||
@@ -789,9 +758,6 @@ impl AdminService {
|
||||
.await
|
||||
{
|
||||
Ok(LoginOutcome::Authenticated(identity)) => Ok(identity),
|
||||
Ok(LoginOutcome::TwoFactorRequired(_)) => Err(ApiError::internal(
|
||||
"two-factor login flow is not configured for this service",
|
||||
)),
|
||||
Err(error) => Err(map_identity_error(error)),
|
||||
};
|
||||
}
|
||||
@@ -1818,204 +1784,6 @@ impl AdminService {
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
async fn start_stream_session_test(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation: &RegistryOperation,
|
||||
runtime: &RuntimeOperation,
|
||||
input: &Value,
|
||||
resolved_auth: Option<&ResolvedAuth>,
|
||||
request_context: &RuntimeRequestContext,
|
||||
) -> Result<StreamSessionStartView, RuntimeError> {
|
||||
let streaming = runtime.execution_config.streaming.as_ref().ok_or_else(|| {
|
||||
RuntimeError::MissingStreamingConfig {
|
||||
operation_id: runtime.operation_id.as_str().to_owned(),
|
||||
}
|
||||
})?;
|
||||
let seed = self
|
||||
.runtime
|
||||
.execute_session_seed_with_auth_and_context(
|
||||
runtime,
|
||||
input,
|
||||
resolved_auth,
|
||||
Some(request_context),
|
||||
)
|
||||
.await?;
|
||||
let batch_size = streaming.max_items.unwrap_or(10).max(1) as usize;
|
||||
let preview_count = seed.items.len().min(batch_size);
|
||||
let preview_items = seed.items[..preview_count].to_vec();
|
||||
let next_index = preview_count;
|
||||
let created_at = OffsetDateTime::now_utc();
|
||||
let expires_at = created_at
|
||||
.checked_add(time::Duration::milliseconds(
|
||||
streaming.max_session_lifetime_ms.unwrap_or(60_000) as i64,
|
||||
))
|
||||
.ok_or_else(|| RuntimeError::SecretCrypto {
|
||||
operation: "compute stream session expiration",
|
||||
details: "failed to compute stream session expiration".to_owned(),
|
||||
})?;
|
||||
let session = StreamSession {
|
||||
id: crank_core::StreamSessionId::new(new_prefixed_id("sess")),
|
||||
workspace_id: workspace_id.clone(),
|
||||
agent_id: None,
|
||||
operation_id: operation.id.clone(),
|
||||
protocol: operation.protocol,
|
||||
mode: ExecutionMode::Session,
|
||||
status: StreamStatus::Running,
|
||||
cursor: (next_index < seed.items.len()).then(|| json!(next_index)),
|
||||
state: json!(StoredSessionState {
|
||||
input: input.clone(),
|
||||
summary: seed.summary.clone(),
|
||||
items: seed.items.clone(),
|
||||
next_index,
|
||||
batch_size,
|
||||
}),
|
||||
expires_at,
|
||||
last_poll_at: None,
|
||||
created_at,
|
||||
closed_at: None,
|
||||
};
|
||||
self.registry
|
||||
.create_stream_session(CreateStreamSessionRequest { session: &session })
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "persist stream session",
|
||||
details: error.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(StreamSessionStartView {
|
||||
session_id: session.id.as_str().to_owned(),
|
||||
status: session.status,
|
||||
expires_at,
|
||||
poll_after_ms: streaming.poll_interval_ms.unwrap_or(1_000),
|
||||
preview: json!({
|
||||
"summary": seed.summary,
|
||||
"items": preview_items,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any())]
|
||||
async fn start_async_job_test(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation: &RegistryOperation,
|
||||
runtime: &RuntimeOperation,
|
||||
input: &Value,
|
||||
request_context: &RuntimeRequestContext,
|
||||
) -> Result<AsyncJobStartView, RuntimeError> {
|
||||
let created_at = OffsetDateTime::now_utc();
|
||||
let streaming = runtime.execution_config.streaming.as_ref().ok_or_else(|| {
|
||||
RuntimeError::MissingStreamingConfig {
|
||||
operation_id: runtime.operation_id.as_str().to_owned(),
|
||||
}
|
||||
})?;
|
||||
let expires_at = created_at
|
||||
.checked_add(time::Duration::milliseconds(
|
||||
streaming.max_session_lifetime_ms.unwrap_or(300_000) as i64,
|
||||
))
|
||||
.ok_or_else(|| RuntimeError::SecretCrypto {
|
||||
operation: "compute async job expiration",
|
||||
details: "failed to compute async job expiration".to_owned(),
|
||||
})?;
|
||||
let job = AsyncJobHandle {
|
||||
id: crank_core::AsyncJobId::new(new_prefixed_id("job")),
|
||||
workspace_id: workspace_id.clone(),
|
||||
agent_id: None,
|
||||
operation_id: operation.id.clone(),
|
||||
status: JobStatus::Running,
|
||||
progress: json!({ "pct": 0 }),
|
||||
result: None,
|
||||
error: None,
|
||||
expires_at: Some(expires_at),
|
||||
last_poll_at: None,
|
||||
created_at,
|
||||
updated_at: created_at,
|
||||
finished_at: None,
|
||||
};
|
||||
self.registry
|
||||
.create_async_job(CreateAsyncJobRequest { job: &job })
|
||||
.await
|
||||
.map_err(|error| RuntimeError::SecretCrypto {
|
||||
operation: "persist async job",
|
||||
details: error.to_string(),
|
||||
})?;
|
||||
|
||||
let registry = self.registry.clone();
|
||||
let secret_crypto = self.secret_crypto.clone();
|
||||
let runtime_for_task = self.runtime.clone();
|
||||
let workspace_for_task = workspace_id.clone();
|
||||
let operation_for_task = runtime.clone();
|
||||
let input_for_task = input.clone();
|
||||
let request_context_for_task = request_context.clone();
|
||||
let job_id = job.id.clone();
|
||||
tokio::spawn(async move {
|
||||
let resolved_auth = resolve_runtime_auth_for_task(
|
||||
®istry,
|
||||
&secret_crypto,
|
||||
&workspace_for_task,
|
||||
&operation_for_task.execution_config,
|
||||
)
|
||||
.await;
|
||||
let result = match resolved_auth {
|
||||
Ok(resolved_auth) => {
|
||||
runtime_for_task
|
||||
.execute_with_auth_and_context(
|
||||
&operation_for_task,
|
||||
&input_for_task,
|
||||
resolved_auth.as_ref(),
|
||||
Some(&request_context_for_task),
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
let finished_at = OffsetDateTime::now_utc();
|
||||
let _ = match result {
|
||||
Ok(output) => {
|
||||
registry
|
||||
.update_async_job_status(UpdateAsyncJobStatusRequest {
|
||||
job_id: &job_id,
|
||||
current_status: JobStatus::Running,
|
||||
next_status: JobStatus::Completed,
|
||||
progress: &json!({ "pct": 100 }),
|
||||
result: Some(&output),
|
||||
error: None,
|
||||
expires_at: None,
|
||||
updated_at: &finished_at,
|
||||
finished_at: Some(&finished_at),
|
||||
})
|
||||
.await
|
||||
}
|
||||
Err(error) => {
|
||||
registry
|
||||
.update_async_job_status(UpdateAsyncJobStatusRequest {
|
||||
job_id: &job_id,
|
||||
current_status: JobStatus::Running,
|
||||
next_status: JobStatus::Failed,
|
||||
progress: &json!({ "pct": 100 }),
|
||||
result: None,
|
||||
error: Some(&json!({
|
||||
"code": runtime_error_code(&error),
|
||||
"message": error.to_string(),
|
||||
})),
|
||||
expires_at: None,
|
||||
updated_at: &finished_at,
|
||||
finished_at: Some(&finished_at),
|
||||
})
|
||||
.await
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Ok(AsyncJobStartView {
|
||||
job_id: job.id.as_str().to_owned(),
|
||||
status: job.status,
|
||||
progress: job.progress,
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_auth_profile(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
@@ -3419,8 +3187,6 @@ impl AdminService {
|
||||
"path": {},
|
||||
"query": {},
|
||||
"headers": { "x-demo-source": "crank-seed" },
|
||||
"variables": null,
|
||||
"grpc": null,
|
||||
"body": demo_rest_request_sample()
|
||||
}),
|
||||
response_preview: demo_rest_response_sample(),
|
||||
@@ -3602,9 +3368,9 @@ fn demo_revops_agent_payload() -> AgentPayload {
|
||||
AgentPayload {
|
||||
slug: "revops-copilot".to_owned(),
|
||||
display_name: "RevOps Copilot".to_owned(),
|
||||
description: "Revenue operations assistant with CRM and billing tools.".to_owned(),
|
||||
description: "Sales operations assistant with CRM tools.".to_owned(),
|
||||
instructions: json!({
|
||||
"system": "Prefer CRM mutations first, then billing lookups for confirmation."
|
||||
"system": "Prefer CRM mutations first, then lookup tools for confirmation."
|
||||
}),
|
||||
tool_selection_policy: json!({
|
||||
"max_tools": 8,
|
||||
|
||||
Reference in New Issue
Block a user