Add approval request HTTP surface
This commit is contained in:
@@ -9,12 +9,12 @@ pub use ext::{ExtensionMigration, RegistryExtension, apply_extension_migrations}
|
||||
|
||||
pub mod records {
|
||||
pub use crate::model::{
|
||||
AgentSummary, AgentVersionRecord, AuthUserRecord, DescriptorKind, DescriptorMetadata,
|
||||
ImportJob, ImportJobId, ImportJobKind, ImportJobStatus, InvitationRecord,
|
||||
InvocationLogRecord, MembershipRecord, OperationAgentRef, OperationSampleMetadata,
|
||||
OperationSummary, OperationUsageSummary, OperationVersionRecord, Page,
|
||||
PlatformApiKeyRecord, PublishedAgentTool, RegistryOperation, SampleKind, SecretRecord,
|
||||
SecretVersionRecord, SessionRecord, UsageAgentBreakdown, UsageBucket,
|
||||
AgentSummary, AgentVersionRecord, ApprovalRequestRecord, AuthUserRecord, DescriptorKind,
|
||||
DescriptorMetadata, ImportJob, ImportJobId, ImportJobKind, ImportJobStatus,
|
||||
InvitationRecord, InvocationLogRecord, MembershipRecord, OperationAgentRef,
|
||||
OperationSampleMetadata, OperationSummary, OperationUsageSummary, OperationVersionRecord,
|
||||
Page, PlatformApiKeyRecord, PublishedAgentTool, RegistryOperation, SampleKind,
|
||||
SecretRecord, SecretVersionRecord, SessionRecord, UsageAgentBreakdown, UsageBucket,
|
||||
UsageOperationBreakdown, UsageRollupRecord, UsageSummary, UsageTimelinePoint,
|
||||
WorkspaceMembershipRecord, WorkspaceRecord, WorkspaceUpstream, WorkspaceUpstreamId,
|
||||
YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
@@ -23,13 +23,14 @@ pub mod records {
|
||||
|
||||
pub mod requests {
|
||||
pub use crate::model::{
|
||||
CreateAgentDraftVersionRequest, CreateAgentRequest, CreateImportJobRequest,
|
||||
CreateInvitationRequest, CreateInvocationLogRequest, CreatePlatformApiKeyRequest,
|
||||
CreateSecretRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||
CreateYamlImportJobRequest, FinishImportJobRequest, ListInvocationLogsQuery,
|
||||
PublishAgentRequest, PublishRequest, RotateSecretRequest, SaveAgentBindingsRequest,
|
||||
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
|
||||
SaveWorkspaceUpstreamRequest, UpdateWorkspaceRequest, UsageQuery,
|
||||
CreateAgentDraftVersionRequest, CreateAgentRequest, CreateApprovalRequest,
|
||||
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
|
||||
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
||||
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
||||
FinishImportJobRequest, ListInvocationLogsQuery, PublishAgentRequest, PublishRequest,
|
||||
RotateSecretRequest, SaveAgentBindingsRequest, SaveAuthProfileRequest,
|
||||
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest,
|
||||
UpdateWorkspaceRequest, UsageQuery,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,15 +40,16 @@ pub mod infrastructure {
|
||||
}
|
||||
|
||||
pub use model::{
|
||||
AgentSummary, AgentVersionRecord, AuthUserRecord, CreateAgentDraftVersionRequest,
|
||||
CreateAgentRequest, CreateImportJobRequest, CreateInvitationRequest,
|
||||
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
|
||||
DescriptorMetadata, FinishImportJobRequest, ImportJob, ImportJobId, ImportJobKind,
|
||||
ImportJobStatus, InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery,
|
||||
MembershipRecord, OperationAgentRef, OperationSampleMetadata, OperationSummary,
|
||||
OperationUsageSummary, OperationVersionRecord, Page, PlatformApiKeyRecord, PublishAgentRequest,
|
||||
PublishRequest, PublishedAgentTool, RegistryOperation, RotateSecretRequest, SampleKind,
|
||||
AgentSummary, AgentVersionRecord, ApprovalRequestRecord, AuthUserRecord,
|
||||
CreateAgentDraftVersionRequest, CreateAgentRequest, CreateApprovalRequest,
|
||||
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
|
||||
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest, CreateWorkspaceRequest,
|
||||
CreateYamlImportJobRequest, DecideApprovalRequest, DescriptorKind, DescriptorMetadata,
|
||||
FinishImportJobRequest, ImportJob, ImportJobId, ImportJobKind, ImportJobStatus,
|
||||
InvitationRecord, InvocationLogRecord, ListInvocationLogsQuery, MembershipRecord,
|
||||
OperationAgentRef, OperationSampleMetadata, OperationSummary, OperationUsageSummary,
|
||||
OperationVersionRecord, Page, PlatformApiKeyRecord, PublishAgentRequest, PublishRequest,
|
||||
PublishedAgentTool, RegistryOperation, RotateSecretRequest, SampleKind,
|
||||
SaveAgentBindingsRequest, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||
SaveSampleMetadataRequest, SaveWorkspaceUpstreamRequest, SecretRecord, SecretVersionRecord,
|
||||
SessionRecord, UpdateWorkspaceRequest, UsageAgentBreakdown, UsageBucket,
|
||||
|
||||
@@ -559,6 +559,35 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create table if not exists approval_requests (
|
||||
id text primary key,
|
||||
workspace_id text not null references workspaces(id) on delete cascade,
|
||||
agent_id text not null references agents(id) on delete cascade,
|
||||
operation_id text not null references operations(id) on delete cascade,
|
||||
operation_version integer not null,
|
||||
status text not null,
|
||||
risk_level text not null,
|
||||
confirmation_title text not null,
|
||||
confirmation_body text not null,
|
||||
request_payload_json jsonb not null,
|
||||
response_payload_json jsonb null,
|
||||
created_at timestamptz not null,
|
||||
expires_at timestamptz not null,
|
||||
decided_at timestamptz null,
|
||||
decided_by_key_id text null references platform_api_keys(id) on delete set null,
|
||||
decision_note text null
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
query(
|
||||
"create index if not exists approval_requests_agent_status_idx
|
||||
on approval_requests(workspace_id, agent_id, status, expires_at)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create table if not exists invocation_logs (
|
||||
id text primary key,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crank_core::{
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, DescriptorId,
|
||||
ExportMode, InvitationToken, InvocationLevel, InvocationLog, InvocationSource, MembershipRole,
|
||||
Operation, OperationId, OperationSecurityLevel, OperationStatus, PlatformApiKey, Protocol,
|
||||
SampleId, Secret, SecretId, SecretVersion, UsagePeriod, UsageRollup, User, UserSessionId,
|
||||
Workspace, WorkspaceId,
|
||||
Agent, AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApprovalRequest,
|
||||
ApprovalRequestId, ApprovalRequestStatus, AuthProfile, DescriptorId, ExportMode,
|
||||
InvitationToken, InvocationLevel, InvocationLog, InvocationSource, MembershipRole, Operation,
|
||||
OperationId, OperationSecurityLevel, OperationStatus, PlatformApiKey, PlatformApiKeyId,
|
||||
Protocol, SampleId, Secret, SecretId, SecretVersion, UsagePeriod, UsageRollup, User,
|
||||
UserSessionId, Workspace, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::MappingSet;
|
||||
use crank_schema::Schema;
|
||||
@@ -95,6 +96,11 @@ pub struct PlatformApiKeyRecord {
|
||||
pub api_key: PlatformApiKey,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ApprovalRequestRecord {
|
||||
pub approval: ApprovalRequest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SecretRecord {
|
||||
pub secret: Secret,
|
||||
@@ -513,6 +519,23 @@ pub struct CreatePlatformApiKeyRequest<'a> {
|
||||
pub secret_hash: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CreateApprovalRequest<'a> {
|
||||
pub approval: &'a ApprovalRequest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DecideApprovalRequest<'a> {
|
||||
pub workspace_id: &'a WorkspaceId,
|
||||
pub agent_id: &'a AgentId,
|
||||
pub approval_id: &'a ApprovalRequestId,
|
||||
pub status: ApprovalRequestStatus,
|
||||
pub decided_at: OffsetDateTime,
|
||||
pub decided_by_key_id: &'a PlatformApiKeyId,
|
||||
pub response_payload: Option<Value>,
|
||||
pub decision_note: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CreateSecretRequest<'a> {
|
||||
pub secret: &'a Secret,
|
||||
|
||||
@@ -170,6 +170,66 @@ impl PostgresRegistry {
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub async fn get_approval_api_key_by_secret_for_agent_slug(
|
||||
&self,
|
||||
workspace_slug: &str,
|
||||
agent_slug: &str,
|
||||
secret_hash: &str,
|
||||
) -> Result<Option<PlatformApiKeyRecord>, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
k.id,
|
||||
k.workspace_id,
|
||||
k.agent_id,
|
||||
k.key_kind,
|
||||
k.name,
|
||||
k.prefix,
|
||||
k.scopes_json,
|
||||
k.status,
|
||||
k.created_at,
|
||||
k.last_used_at,
|
||||
k.expires_at,
|
||||
k.allowed_origins_json
|
||||
from platform_api_keys k
|
||||
join workspaces w on w.id = k.workspace_id
|
||||
join agents a on a.id = k.agent_id
|
||||
where w.slug = $1
|
||||
and a.slug = $2
|
||||
and k.secret_hash = $3
|
||||
and k.key_kind = 'approval'
|
||||
and k.status = 'active'
|
||||
and (k.expires_at is null or k.expires_at > now())
|
||||
limit 1",
|
||||
)
|
||||
.bind(workspace_slug)
|
||||
.bind(agent_slug)
|
||||
.bind(secret_hash)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
row.map(|row| {
|
||||
Ok(PlatformApiKeyRecord {
|
||||
api_key: PlatformApiKey {
|
||||
id: PlatformApiKeyId::new(row.get::<String, _>("id")),
|
||||
workspace_id: WorkspaceId::new(row.get::<String, _>("workspace_id")),
|
||||
agent_id: row.get::<Option<String>, _>("agent_id").map(AgentId::new),
|
||||
key_kind: deserialize_enum_text(&row.get::<String, _>("key_kind"), "key_kind")?,
|
||||
name: row.get("name"),
|
||||
prefix: row.get("prefix"),
|
||||
scopes: deserialize_json_value(row.get::<Value, _>("scopes_json"))?,
|
||||
status: deserialize_enum_text(&row.get::<String, _>("status"), "status")?,
|
||||
created_at: row.get("created_at"),
|
||||
last_used_at: row.get("last_used_at"),
|
||||
expires_at: row.get("expires_at"),
|
||||
allowed_origins: deserialize_json_value(
|
||||
row.get::<Value, _>("allowed_origins_json"),
|
||||
)?,
|
||||
},
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
pub async fn create_platform_api_key(
|
||||
&self,
|
||||
request: CreatePlatformApiKeyRequest<'_>,
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
use super::*;
|
||||
|
||||
impl PostgresRegistry {
|
||||
pub async fn create_approval_request(
|
||||
&self,
|
||||
request: CreateApprovalRequest<'_>,
|
||||
) -> Result<(), RegistryError> {
|
||||
sqlx::query(
|
||||
"insert into approval_requests (
|
||||
id,
|
||||
workspace_id,
|
||||
agent_id,
|
||||
operation_id,
|
||||
operation_version,
|
||||
status,
|
||||
risk_level,
|
||||
confirmation_title,
|
||||
confirmation_body,
|
||||
request_payload_json,
|
||||
response_payload_json,
|
||||
created_at,
|
||||
expires_at,
|
||||
decided_at,
|
||||
decided_by_key_id,
|
||||
decision_note
|
||||
) values (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
|
||||
$11, $12::timestamptz, $13::timestamptz, $14::timestamptz,
|
||||
$15, $16
|
||||
)",
|
||||
)
|
||||
.bind(request.approval.id.as_str())
|
||||
.bind(request.approval.workspace_id.as_str())
|
||||
.bind(request.approval.agent_id.as_str())
|
||||
.bind(request.approval.operation_id.as_str())
|
||||
.bind(to_db_version(request.approval.operation_version))
|
||||
.bind(serialize_enum_text(
|
||||
&request.approval.status,
|
||||
"approval_status",
|
||||
)?)
|
||||
.bind(serialize_enum_text(
|
||||
&request.approval.risk_level,
|
||||
"approval_risk_level",
|
||||
)?)
|
||||
.bind(&request.approval.confirmation_title)
|
||||
.bind(&request.approval.confirmation_body)
|
||||
.bind(Json(&request.approval.request_payload))
|
||||
.bind(request.approval.response_payload.as_ref().map(Json))
|
||||
.bind(request.approval.created_at)
|
||||
.bind(request.approval.expires_at)
|
||||
.bind(request.approval.decided_at)
|
||||
.bind(
|
||||
request
|
||||
.approval
|
||||
.decided_by_key_id
|
||||
.as_ref()
|
||||
.map(PlatformApiKeyId::as_str),
|
||||
)
|
||||
.bind(request.approval.decision_note.as_deref())
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_pending_approval_requests_for_agent(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
) -> Result<Vec<ApprovalRequestRecord>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
agent_id,
|
||||
operation_id,
|
||||
operation_version,
|
||||
status,
|
||||
risk_level,
|
||||
confirmation_title,
|
||||
confirmation_body,
|
||||
request_payload_json,
|
||||
response_payload_json,
|
||||
created_at,
|
||||
expires_at,
|
||||
decided_at,
|
||||
decided_by_key_id,
|
||||
decision_note
|
||||
from approval_requests
|
||||
where workspace_id = $1
|
||||
and agent_id = $2
|
||||
and status = 'pending'
|
||||
and expires_at > now()
|
||||
order by created_at asc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(agent_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
rows.into_iter().map(map_approval_request_row).collect()
|
||||
}
|
||||
|
||||
pub async fn get_approval_request_for_agent(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
agent_id: &AgentId,
|
||||
approval_id: &ApprovalRequestId,
|
||||
) -> Result<Option<ApprovalRequestRecord>, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
agent_id,
|
||||
operation_id,
|
||||
operation_version,
|
||||
status,
|
||||
risk_level,
|
||||
confirmation_title,
|
||||
confirmation_body,
|
||||
request_payload_json,
|
||||
response_payload_json,
|
||||
created_at,
|
||||
expires_at,
|
||||
decided_at,
|
||||
decided_by_key_id,
|
||||
decision_note
|
||||
from approval_requests
|
||||
where workspace_id = $1
|
||||
and agent_id = $2
|
||||
and id = $3
|
||||
limit 1",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(agent_id.as_str())
|
||||
.bind(approval_id.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
row.map(map_approval_request_row).transpose()
|
||||
}
|
||||
|
||||
pub async fn decide_approval_request(
|
||||
&self,
|
||||
request: DecideApprovalRequest<'_>,
|
||||
) -> Result<Option<ApprovalRequestRecord>, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"update approval_requests
|
||||
set status = $1,
|
||||
response_payload_json = $2,
|
||||
decided_at = $3::timestamptz,
|
||||
decided_by_key_id = $4,
|
||||
decision_note = $5
|
||||
where workspace_id = $6
|
||||
and agent_id = $7
|
||||
and id = $8
|
||||
and status = 'pending'
|
||||
and expires_at > $3::timestamptz
|
||||
returning
|
||||
id,
|
||||
workspace_id,
|
||||
agent_id,
|
||||
operation_id,
|
||||
operation_version,
|
||||
status,
|
||||
risk_level,
|
||||
confirmation_title,
|
||||
confirmation_body,
|
||||
request_payload_json,
|
||||
response_payload_json,
|
||||
created_at,
|
||||
expires_at,
|
||||
decided_at,
|
||||
decided_by_key_id,
|
||||
decision_note",
|
||||
)
|
||||
.bind(serialize_enum_text(&request.status, "approval_status")?)
|
||||
.bind(request.response_payload.as_ref().map(Json))
|
||||
.bind(request.decided_at)
|
||||
.bind(request.decided_by_key_id.as_str())
|
||||
.bind(request.decision_note)
|
||||
.bind(request.workspace_id.as_str())
|
||||
.bind(request.agent_id.as_str())
|
||||
.bind(request.approval_id.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
row.map(map_approval_request_row).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
fn map_approval_request_row(row: PgRow) -> Result<ApprovalRequestRecord, RegistryError> {
|
||||
Ok(ApprovalRequestRecord {
|
||||
approval: ApprovalRequest {
|
||||
id: ApprovalRequestId::new(row.get::<String, _>("id")),
|
||||
workspace_id: WorkspaceId::new(row.get::<String, _>("workspace_id")),
|
||||
agent_id: AgentId::new(row.get::<String, _>("agent_id")),
|
||||
operation_id: OperationId::new(row.get::<String, _>("operation_id")),
|
||||
operation_version: from_db_version(row.get("operation_version"), "operation_version")?,
|
||||
status: deserialize_enum_text(&row.get::<String, _>("status"), "approval_status")?,
|
||||
risk_level: deserialize_enum_text(
|
||||
&row.get::<String, _>("risk_level"),
|
||||
"approval_risk_level",
|
||||
)?,
|
||||
confirmation_title: row.get("confirmation_title"),
|
||||
confirmation_body: row.get("confirmation_body"),
|
||||
request_payload: row.get::<Value, _>("request_payload_json"),
|
||||
response_payload: row.get::<Option<Value>, _>("response_payload_json"),
|
||||
created_at: row.get("created_at"),
|
||||
expires_at: row.get("expires_at"),
|
||||
decided_at: row.get("decided_at"),
|
||||
decided_by_key_id: row
|
||||
.get::<Option<String>, _>("decided_by_key_id")
|
||||
.map(PlatformApiKeyId::new),
|
||||
decision_note: row.get("decision_note"),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod agent;
|
||||
mod api_key;
|
||||
mod approval;
|
||||
mod auth;
|
||||
mod connection;
|
||||
mod import_job;
|
||||
@@ -13,10 +14,11 @@ mod workspace;
|
||||
mod yaml_import;
|
||||
|
||||
use crank_core::{
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, AuthProfile, HttpMethod,
|
||||
InvitationId, InvitationToken, InvocationLog, InvocationLogId, MembershipRole, OperationId,
|
||||
OperationStatus, PlatformApiKey, PlatformApiKeyId, PlatformApiKeyStatus, Secret, SecretId,
|
||||
SecretVersion, Target, UsageRollup, User, UserId, UserSessionId, Workspace, WorkspaceId,
|
||||
AgentId, AgentOperationBinding, AgentStatus, AgentVersion, ApprovalRequest, ApprovalRequestId,
|
||||
AuthProfile, HttpMethod, InvitationId, InvitationToken, InvocationLog, InvocationLogId,
|
||||
MembershipRole, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId,
|
||||
PlatformApiKeyStatus, Secret, SecretId, SecretVersion, Target, UsageRollup, User, UserId,
|
||||
UserSessionId, Workspace, WorkspaceId,
|
||||
};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::Value;
|
||||
@@ -28,10 +30,11 @@ pub use pool_config::{PostgresPoolConfig, PostgresPoolConfigError};
|
||||
use crate::{
|
||||
error::RegistryError,
|
||||
model::{
|
||||
AgentSummary, AgentVersionRecord, AuthUserRecord, CreateAgentDraftVersionRequest,
|
||||
CreateAgentRequest, CreateImportJobRequest, CreateInvitationRequest,
|
||||
CreateInvocationLogRequest, CreatePlatformApiKeyRequest, CreateSecretRequest,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
|
||||
AgentSummary, AgentVersionRecord, ApprovalRequestRecord, AuthUserRecord,
|
||||
CreateAgentDraftVersionRequest, CreateAgentRequest, CreateApprovalRequest,
|
||||
CreateImportJobRequest, CreateInvitationRequest, CreateInvocationLogRequest,
|
||||
CreatePlatformApiKeyRequest, CreateSecretRequest, CreateVersionRequest,
|
||||
CreateWorkspaceRequest, CreateYamlImportJobRequest, DecideApprovalRequest,
|
||||
DescriptorMetadata, FinishImportJobRequest, ImportJob, ImportJobId, InvitationRecord,
|
||||
InvocationLogRecord, ListInvocationLogsQuery, MembershipRecord, OperationAgentRef,
|
||||
OperationSampleMetadata, OperationSummary, OperationUsageSummary, OperationVersionRecord,
|
||||
|
||||
Reference in New Issue
Block a user