Усилить безопасность и надёжность выполнения операций
CI / Rust Checks (push) Successful in 5m7s
CI / UI Checks (push) Successful in 4s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 3m9s
CI / Deploy (push) Successful in 1m41s

This commit is contained in:
2026-07-11 14:08:07 +03:00
parent 626f2845e2
commit 8318e4b560
40 changed files with 1343 additions and 185 deletions
+1
View File
@@ -11,6 +11,7 @@ pub enum ApprovalRequestStatus {
#[default]
Pending,
Approved,
Executing,
Denied,
Expired,
Completed,
+4 -3
View File
@@ -38,8 +38,8 @@ pub mod domain {
UserSessionId, WorkspaceId,
};
pub use crate::observability::{
InvocationLevel, InvocationLog, InvocationSource, InvocationStatus, UsagePeriod,
UsageRollup,
INVOCATION_PREVIEW_MAX_BYTES, InvocationLevel, InvocationLog, InvocationSource,
InvocationStatus, UsagePeriod, UsageRollup, sanitize_invocation_preview,
};
pub use crate::operation::{
ConfigExport, ConfirmationPolicy, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus,
@@ -129,7 +129,8 @@ pub use ids::{
UserSessionId, WorkspaceId,
};
pub use observability::{
InvocationLevel, InvocationLog, InvocationSource, InvocationStatus, UsagePeriod, UsageRollup,
INVOCATION_PREVIEW_MAX_BYTES, InvocationLevel, InvocationLog, InvocationSource,
InvocationStatus, UsagePeriod, UsageRollup, sanitize_invocation_preview,
};
pub use operation::{
ConfigExport, ConfirmationPolicy, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus,
+93 -2
View File
@@ -1,9 +1,68 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_json::{Map, Value, json};
use time::OffsetDateTime;
use crate::{AgentId, OperationId, WorkspaceId};
pub const INVOCATION_PREVIEW_MAX_BYTES: usize = 16 * 1024;
pub fn sanitize_invocation_preview(value: &Value) -> Value {
let redacted = redact_sensitive_fields(value);
let Ok(encoded) = serde_json::to_vec(&redacted) else {
return Value::Null;
};
if encoded.len() <= INVOCATION_PREVIEW_MAX_BYTES {
return redacted;
}
let preview = String::from_utf8_lossy(&encoded[..INVOCATION_PREVIEW_MAX_BYTES]).into_owned();
json!({
"truncated": true,
"original_bytes": encoded.len(),
"preview": preview,
})
}
fn redact_sensitive_fields(value: &Value) -> Value {
match value {
Value::Object(object) => Value::Object(
object
.iter()
.map(|(key, value)| {
let value = if is_sensitive_key(key) {
Value::String("[REDACTED]".to_owned())
} else {
redact_sensitive_fields(value)
};
(key.clone(), value)
})
.collect::<Map<String, Value>>(),
),
Value::Array(items) => Value::Array(items.iter().map(redact_sensitive_fields).collect()),
_ => value.clone(),
}
}
fn is_sensitive_key(key: &str) -> bool {
let normalized = key.to_ascii_lowercase().replace('-', "_");
let compact = normalized
.chars()
.filter(char::is_ascii_alphanumeric)
.collect::<String>();
[
"authorization",
"cookie",
"password",
"passwd",
"secret",
"token",
]
.iter()
.any(|sensitive| compact.contains(sensitive))
|| ["apikey", "accesskey", "privatekey"]
.iter()
.any(|sensitive| compact.contains(sensitive))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InvocationSource {
@@ -87,7 +146,10 @@ mod tests {
use serde_json::json;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use super::{InvocationLevel, InvocationLog, InvocationSource, InvocationStatus, UsagePeriod};
use super::{
INVOCATION_PREVIEW_MAX_BYTES, InvocationLevel, InvocationLog, InvocationSource,
InvocationStatus, UsagePeriod, sanitize_invocation_preview,
};
use crate::{AgentId, OperationId, WorkspaceId, ids::InvocationLogId};
fn timestamp(value: &str) -> OffsetDateTime {
@@ -129,4 +191,33 @@ mod tests {
assert_eq!(value, "7d");
}
#[test]
fn redacts_sensitive_fields_recursively() {
let preview = sanitize_invocation_preview(&json!({
"authorization": "Bearer secret",
"nested": {
"api_key": "key",
"refreshToken": "token",
"client_secret_value": "secret",
"value": 42
}
}));
assert_eq!(preview["authorization"], "[REDACTED]");
assert_eq!(preview["nested"]["api_key"], "[REDACTED]");
assert_eq!(preview["nested"]["refreshToken"], "[REDACTED]");
assert_eq!(preview["nested"]["client_secret_value"], "[REDACTED]");
assert_eq!(preview["nested"]["value"], 42);
}
#[test]
fn truncates_large_previews() {
let preview = sanitize_invocation_preview(&json!({
"body": "x".repeat(INVOCATION_PREVIEW_MAX_BYTES * 2)
}));
assert_eq!(preview["truncated"], true);
assert!(preview["original_bytes"].as_u64().unwrap() > INVOCATION_PREVIEW_MAX_BYTES as u64);
}
}